From 37e7b5179d34e6fb93926897a6a1ad735de62237 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 7 Sep 2022 20:24:35 +0200 Subject: [PATCH] add person business object at db level --- .../hs/admin/person/HsAdminPersonEntity.java | 20 +- .../admin/person/HsAdminPersonRepository.java | 4 +- .../hs/admin/person/HsAdminPersonType.java | 8 + .../resources/db/changelog/010-context.sql | 6 +- .../resources/db/changelog/055-rbac-views.sql | 2 +- .../db/changelog/080-rbac-global.sql | 1 + .../db/changelog/210-hs-admin-person.sql | 19 ++ .../db/changelog/213-hs-admin-person-rbac.sql | 286 ++++++++++++++++++ .../218-hs-admin-person-test-data.sql | 69 +++++ .../db/changelog/db.changelog-master.yaml | 9 +- ...AdminContactRepositoryIntegrationTest.java | 38 ++- .../hs/admin/partner/TestHsAdminPartner.java | 5 +- ...sAdminPersonRepositoryIntegrationTest.java | 227 ++++++++++++++ .../hs/admin/person/TestHsAdminPerson.java | 16 + .../RbacGrantControllerAcceptanceTest.java | 4 +- .../RbacRoleControllerAcceptanceTest.java | 18 +- tools/generate | 11 + 17 files changed, 707 insertions(+), 36 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonType.java create mode 100644 src/main/resources/db/changelog/210-hs-admin-person.sql create mode 100644 src/main/resources/db/changelog/213-hs-admin-person-rbac.sql create mode 100644 src/main/resources/db/changelog/218-hs-admin-person-test-data.sql create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepositoryIntegrationTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/admin/person/TestHsAdminPerson.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonEntity.java index f64965d9..af94768e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonEntity.java @@ -1,7 +1,9 @@ package net.hostsharing.hsadminng.hs.admin.person; -import com.vladmihalcea.hibernate.type.array.ListArrayType; +import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; import lombok.*; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import javax.persistence.*; @@ -10,8 +12,8 @@ import java.util.UUID; @Entity @Table(name = "hs_admin_person_rv") @TypeDef( - name = "list-array", - typeClass = ListArrayType.class + name = "pgsql_enum", + typeClass = PostgreSQLEnumType.class ) @Getter @Setter @@ -22,9 +24,12 @@ public class HsAdminPersonEntity { private @Id UUID uuid; + @Column(name = "persontype") @Enumerated(EnumType.STRING) - private PersonType type; + @Type( type = "pgsql_enum" ) + private HsAdminPersonType personType; + @Column(name = "tradename") private String tradeName; @Column(name = "givenname") @@ -33,10 +38,7 @@ public class HsAdminPersonEntity { @Column(name = "familyname") private String familyName; - public enum PersonType { - NATURAL, - LEGAL, - SOLE_REPRESENTATION, - JOINT_REPRESENTATION + public String getDisplayName() { + return !StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepository.java index f702a4b6..6150a32a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepository.java @@ -9,7 +9,7 @@ import java.util.UUID; public interface HsAdminPersonRepository extends Repository { - Optional findByUuid(UUID id); + Optional findByUuid(UUID personUuid); @Query(""" SELECT p FROM HsAdminPersonEntity p @@ -22,5 +22,7 @@ public interface HsAdminPersonRepository extends Repository 'INSERT' then + raise exception 'invalid usage of TRIGGER AFTER INSERT'; + end if; + + -- the owner role with full access for the creator assigned to the current user + ownerRole = createRole( + hsAdminPersonOwner(NEW), + grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['*']), + beneathRole(globalAdmin()), + withoutSubRoles(), + withUser(currentUser()), -- TODO.spec: Who is owner of a new person? + grantedByRole(globalAdmin()) + ); + + -- the tenant role for those related users who can view the data + perform createRole( + hsAdminPersonTenant(NEW), + grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']), + beneathRole(ownerRole) + ); + + return NEW; +end; $$; + +/* + An AFTER INSERT TRIGGER which creates the role structure for a new customer. + */ + +create trigger createRbacRolesForHsAdminPerson_Trigger + after insert + on hs_admin_person + for each row +execute procedure createRbacRolesForHsAdminPerson(); +--// + + +-- ============================================================================ +--changeset hs-admin-person-rbac-ROLES-REMOVAL:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Deletes the roles and their assignments of a deleted person for the BEFORE DELETE TRIGGER. + */ +create or replace function deleteRbacRulesForHsAdminPerson() + returns trigger + language plpgsql + strict as $$ +begin + if TG_OP = 'DELETE' then + call deleteRole(findRoleId(hsAdminPersonOwner(OLD))); + call deleteRole(findRoleId(hsAdminPersonTenant(OLD))); + else + raise exception 'invalid usage of TRIGGER BEFORE DELETE'; + end if; + return old; +end; $$; + +/* + An BEFORE DELETE TRIGGER which deletes the role structure of a person. + */ +create trigger deleteRbacRulesForTestPerson_Trigger + before delete + on hs_admin_person + for each row +execute procedure deleteRbacRulesForHsAdminPerson(); +--// + +-- ============================================================================ +--changeset hs-admin-person-rbac-IDENTITY-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates a view to the person main table which maps the identifying name + (in this case, the prefix) to the objectUuid. + */ +create or replace view hs_admin_person_iv as +select target.uuid, cleanIdentifier(concat(target.tradeName, target.familyName, target.givenName)) as idName + from hs_admin_person as target; +-- TODO.spec: Is it ok that everybody has access to this information? +grant all privileges on hs_admin_person_iv to restricted; + +/* + Returns the objectUuid for a given identifying name (in this case the prefix). + */ +create or replace function hs_admin_personUuidByIdName(idName varchar) + returns uuid + language sql + strict as $$ +select uuid from hs_admin_person_iv iv where iv.idName = hs_admin_personUuidByIdName.idName; +$$; + +/* + Returns the identifying name for a given objectUuid (in this case the label). + */ +create or replace function hs_admin_personIdNameByUuid(uuid uuid) + returns varchar + language sql + strict as $$ +select idName from hs_admin_person_iv iv where iv.uuid = hs_admin_personIdNameByUuid.uuid; +$$; +--// + + +-- ============================================================================ +--changeset hs-admin-person-rbac-RESTRICTED-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +/* + Creates a view to the person main table with row-level limitation + based on the 'view' permission of the current user or assumed roles. + */ +set session session authorization default; +drop view if exists hs_admin_person_rv; +create or replace view hs_admin_person_rv as +select target.* + from hs_admin_person as target + where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'hs_admin_person', currentSubjectsUuids())); +grant all privileges on hs_admin_person_rv to restricted; +--// + + +-- ============================================================================ +--changeset hs-admin-person-rbac-INSTEAD-OF-INSERT-TRIGGER:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/** + Instead of insert trigger function for hs_admin_person_rv. + */ +create or replace function insertHsAdminPerson() + returns trigger + language plpgsql as $$ +declare + newUser hs_admin_person; +begin + insert + into hs_admin_person + values (new.*) + returning * into newUser; + return newUser; +end; +$$; + +/* + Creates an instead of insert trigger for the hs_admin_person_rv view. + */ +create trigger insertHsAdminPerson_Trigger + instead of insert + on hs_admin_person_rv + for each row +execute function insertHsAdminPerson(); +--// + +-- ============================================================================ +--changeset hs-admin-person-rbac-INSTEAD-OF-DELETE-TRIGGER:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/** + Instead of delete trigger function for hs_admin_person_rv. + */ +create or replace function deleteHsAdminPerson() + returns trigger + language plpgsql as $$ +begin + if true or hasGlobalRoleGranted(currentUserUuid()) or + old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('delete', 'hs_admin_person', currentSubjectsUuids())) then + delete from hs_admin_person c where c.uuid = old.uuid; + return old; + end if; + raise exception '[403] User % not allowed to delete person uuid %', currentUser(), old.uuid; +end; $$; + +/* + Creates an instead of delete trigger for the hs_admin_person_rv view. + */ +create trigger deleteHsAdminPerson_Trigger + instead of delete + on hs_admin_person_rv + for each row +execute function deleteHsAdminPerson(); +--/ + +-- ============================================================================ +--changeset hs-admin-person-rbac-NEW-PERSON:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +/* + Creates a global permission for new-person and assigns it to the hostsharing admins role. + */ +do language plpgsql $$ + declare + addCustomerPermissions uuid[]; + globalObjectUuid uuid; + globalAdminRoleUuid uuid ; + begin + call defineContext('granting global new-person permission to global admin role', null, null, null); + + globalAdminRoleUuid := findRoleId(globalAdmin()); + globalObjectUuid := (select uuid from global); + addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-person']); + call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions); + end; +$$; + +/** + Used by the trigger to prevent the add-customer to current user respectively assumed roles. + */ +create or replace function addHsAdminPersonNotAllowedForCurrentSubjects() + returns trigger + language PLPGSQL +as $$ +begin + raise exception '[403] new-person not permitted for %', + array_to_string(currentSubjects(), ';', 'null'); +end; $$; + +/** + Checks if the user or assumed roles are allowed to create a new customer. + */ +create trigger hs_admin_person_insert_trigger + before insert + on hs_admin_person + for each row + -- TODO.spec: who is allowed to create new persons + when ( not hasAssumedRole() ) +execute procedure addHsAdminPersonNotAllowedForCurrentSubjects(); +--// + diff --git a/src/main/resources/db/changelog/218-hs-admin-person-test-data.sql b/src/main/resources/db/changelog/218-hs-admin-person-test-data.sql new file mode 100644 index 00000000..e9fc7d62 --- /dev/null +++ b/src/main/resources/db/changelog/218-hs-admin-person-test-data.sql @@ -0,0 +1,69 @@ +--liquibase formatted sql + + +-- ============================================================================ +--changeset hs-admin-person-TEST-DATA-GENERATOR:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates a single person test record. + */ +create or replace procedure createHsAdminPersonTestData( + personType HsAdminPersonType, + tradeName varchar, + familyName varchar = null, + givenName varchar = null +) + language plpgsql as $$ +declare + fullName varchar; + currentTask varchar; + emailAddr varchar; +begin + fullName := concat_ws(', ', personType, tradename, familyName, givenName); + currentTask = 'creating RBAC test person ' || fullName; + emailAddr = 'person-' || left(cleanIdentifier(fullName), 32) || '@example.com'; + call defineContext(currentTask); + perform createRbacUser(emailAddr); + call defineContext(currentTask, null, emailAddr); + execute format('set local hsadminng.currentTask to %L', currentTask); + + raise notice 'creating test person: %', fullName; + insert + into hs_admin_person (persontype, tradename, givenname, familyname) + values (personType, tradeName, givenName, familyName); +end; $$; +--// + +/* + Creates a range of test persons for mass data generation. + */ +create or replace procedure createTestPersonTestData( + startCount integer, -- count of auto generated rows before the run + endCount integer -- count of auto generated rows after the run +) + language plpgsql as $$ +begin + for t in startCount..endCount + loop + call createHsAdminPersonTestData('LEGAL', intToVarChar(t, 4)); + commit; + end loop; +end; $$; +--// + + +-- ============================================================================ +--changeset hs-admin-person-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--// +-- ---------------------------------------------------------------------------- + +do language plpgsql $$ + begin + call createHsAdminPersonTestData('LEGAL', 'first person'); + call createHsAdminPersonTestData('NATURAL', null, 'Peter', 'Smith'); + call createHsAdminPersonTestData('LEGAL', 'Rockshop e.K.', 'Sandra', 'Miller'); + call createHsAdminPersonTestData('SOLE_REPRESENTATION', 'Ostfriesische Stahlhandel OHG'); + call createHsAdminPersonTestData('JOINT_REPRESENTATION', 'Erbengemeinschaft Bessler', 'Mel', 'Bessler'); + end; +$$; +--// diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index cbf1161b..447efccd 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -51,6 +51,9 @@ databaseChangeLog: file: db/changelog/203-hs-admin-contact-rbac.sql - include: file: db/changelog/208-hs-admin-contact-test-data.sql - - - + - include: + file: db/changelog/210-hs-admin-person.sql + - include: + file: db/changelog/213-hs-admin-person-rbac.sql + - include: + file: db/changelog/218-hs-admin-person-test-data.sql diff --git a/src/test/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactRepositoryIntegrationTest.java index 32cdb7cf..5c633734 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactRepositoryIntegrationTest.java @@ -3,8 +3,10 @@ package net.hostsharing.hsadminng.hs.admin.contact; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.ContextBasedTest; import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.modelmapper.internal.bytebuddy.utility.RandomString; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -52,7 +54,7 @@ class HsAdminContactRepositoryIntegrationTest extends ContextBasedTest { hsAdminContact("a new contact", "contact-admin@www.example.com"))); // then - assertThat(result.wasSuccessful()).isTrue(); + result.assertSuccessful(); assertThat(result.returnedValue()).isNotNull().extracting(HsAdminContactEntity::getUuid).isNotNull(); assertThatContactIsPersisted(result.returnedValue()); assertThat(contactRepo.count()).isEqualTo(count + 1); @@ -61,7 +63,7 @@ class HsAdminContactRepositoryIntegrationTest extends ContextBasedTest { @Test public void arbitraryUser_canCreateNewContact() { // given - context("pac-admin-xxx00@xxx.example.com"); + context("drew@hostsharing.org"); final var count = contactRepo.count(); // when @@ -69,7 +71,7 @@ class HsAdminContactRepositoryIntegrationTest extends ContextBasedTest { hsAdminContact("another new contact", "another-new-contact@example.com"))); // then - assertThat(result.wasSuccessful()).isTrue(); + result.assertSuccessful(); assertThat(result.returnedValue()).isNotNull().extracting(HsAdminContactEntity::getUuid).isNotNull(); assertThatContactIsPersisted(result.returnedValue()); assertThat(contactRepo.count()).isEqualTo(count + 1); @@ -99,10 +101,10 @@ class HsAdminContactRepositoryIntegrationTest extends ContextBasedTest { @Test public void arbitraryUser_canViewOnlyItsOwnContact() { // given: - final var givenContact = givenSomeTemporaryContact("pac-admin-xxx00@xxx.example.com"); + final var givenContact = givenSomeTemporaryContact("drew@hostsharing.org"); // when: - context("pac-admin-xxx00@xxx.example.com"); + context("drew@hostsharing.org"); final var result = contactRepo.findContactByOptionalLabelLike(null); // then: @@ -128,10 +130,10 @@ class HsAdminContactRepositoryIntegrationTest extends ContextBasedTest { @Test public void arbitraryUser_withoutAssumedRole_canViewOnlyItsOwnContact() { // given: - final var givenContact = givenSomeTemporaryContact("pac-admin-xxx00@xxx.example.com"); + final var givenContact = givenSomeTemporaryContact("drew@hostsharing.org"); // when: - context("pac-admin-xxx00@xxx.example.com"); + context("drew@hostsharing.org"); final var result = contactRepo.findContactByOptionalLabelLike(givenContact.getLabel()); // then: @@ -145,7 +147,7 @@ class HsAdminContactRepositoryIntegrationTest extends ContextBasedTest { @Test public void globalAdmin_withoutAssumedRole_canDeleteAnyContact() { // given - final var givenContact = givenSomeTemporaryContact("pac-admin-xxx00@xxx.example.com"); + final var givenContact = givenSomeTemporaryContact("drew@hostsharing.org"); // when final var result = jpaAttempt.transacted(() -> { @@ -164,11 +166,11 @@ class HsAdminContactRepositoryIntegrationTest extends ContextBasedTest { @Test public void arbitraryUser_withoutAssumedRole_canDeleteAContactCreatedByItself() { // given - final var givenContact = givenSomeTemporaryContact("pac-admin-xxx00@xxx.example.com"); + final var givenContact = givenSomeTemporaryContact("drew@hostsharing.org"); // when final var result = jpaAttempt.transacted(() -> { - context("pac-admin-xxx00@xxx.example.com", null); + context("drew@hostsharing.org", null); contactRepo.deleteByUuid(givenContact.getUuid()); }); @@ -190,16 +192,26 @@ class HsAdminContactRepositoryIntegrationTest extends ContextBasedTest { }).assumeSuccessful().returnedValue(); } + @AfterEach + void cleanup() { + context("alex@hostsharing.net", null); + final var result = contactRepo.findContactByOptionalLabelLike("some temporary contact"); + result.forEach(tempPerson -> { + System.out.println("DELETING contact: " + tempPerson.getLabel()); + contactRepo.deleteByUuid(tempPerson.getUuid()); + }); + } + private HsAdminContactEntity givenSomeTemporaryContact(final String createdByUser) { + final var random = RandomString.make(12); return givenSomeTemporaryContact(createdByUser, () -> hsAdminContact( - "some temporary contact #" + Math.random(), - "some-temporary-contact" + Math.random() + "@example.com")); + "some temporary contact #" + random, + "some-temporary-contact" + random + "@example.com")); } void exactlyTheseContactsAreReturned(final List actualResult, final String... contactLabels) { assertThat(actualResult) - .hasSize(contactLabels.length) .extracting(HsAdminContactEntity::getLabel) .containsExactlyInAnyOrder(contactLabels); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/admin/partner/TestHsAdminPartner.java b/src/test/java/net/hostsharing/hsadminng/hs/admin/partner/TestHsAdminPartner.java index 3f09ea14..72b8e48b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/admin/partner/TestHsAdminPartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/admin/partner/TestHsAdminPartner.java @@ -1,12 +1,11 @@ package net.hostsharing.hsadminng.hs.admin.partner; import net.hostsharing.hsadminng.hs.admin.contact.HsAdminContactEntity; -import net.hostsharing.hsadminng.hs.admin.partner.HsAdminPartnerEntity; import net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonEntity; import java.util.UUID; -import static net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonEntity.PersonType.LEGAL; +import static net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonType.LEGAL; public class TestHsAdminPartner { @@ -16,7 +15,7 @@ public class TestHsAdminPartner { return HsAdminPartnerEntity.builder() .uuid(UUID.randomUUID()) .person(HsAdminPersonEntity.builder() - .type(LEGAL) + .personType(LEGAL) .tradeName(tradeName) .build()) .contact(HsAdminContactEntity.builder() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepositoryIntegrationTest.java new file mode 100644 index 00000000..839b7a1c --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepositoryIntegrationTest.java @@ -0,0 +1,227 @@ +package net.hostsharing.hsadminng.hs.admin.person; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.context.ContextBasedTest; +import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.modelmapper.internal.bytebuddy.utility.RandomString; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.test.annotation.DirtiesContext; + +import javax.persistence.EntityManager; +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Random; +import java.util.function.Supplier; +import java.util.random.RandomGenerator; + +import static net.hostsharing.hsadminng.hs.admin.person.TestHsAdminPerson.hsAdminPerson; +import static net.hostsharing.test.JpaAttempt.attempt; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ComponentScan(basePackageClasses = { HsAdminPersonRepository.class, Context.class, JpaAttempt.class }) +@DirtiesContext +class HsAdminPersonRepositoryIntegrationTest extends ContextBasedTest { + + @Autowired + HsAdminPersonRepository personRepo; + + @Autowired + EntityManager em; + + @Autowired + JpaAttempt jpaAttempt; + + @MockBean + HttpServletRequest request; + + @Nested + class CreatePerson { + + @Test + public void globalAdmin_withoutAssumedRole_canCreateNewPerson() { + // given + context("alex@hostsharing.net"); + final var count = personRepo.count(); + + // when + + final var result = attempt(em, () -> personRepo.save( + hsAdminPerson("a new person"))); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsAdminPersonEntity::getUuid).isNotNull(); + assertThatPersonIsPersisted(result.returnedValue()); + assertThat(personRepo.count()).isEqualTo(count + 1); + } + + @Test + public void arbitraryUser_canCreateNewPerson() { + // given + context("drew@hostsharing.org"); + final var count = personRepo.count(); + + // when + final var result = attempt(em, () -> personRepo.save( + hsAdminPerson("another new person"))); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsAdminPersonEntity::getUuid).isNotNull(); + assertThatPersonIsPersisted(result.returnedValue()); + assertThat(personRepo.count()).isEqualTo(count + 1); + } + + private void assertThatPersonIsPersisted(final HsAdminPersonEntity saved) { + final var found = personRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); + } + } + + @Nested + class FindAllPersons { + + @Test + public void globalAdmin_withoutAssumedRole_canViewAllPersons() { + // given + context("alex@hostsharing.net"); + + // when + final var result = personRepo.findPersonByOptionalNameLike(null); + + // then + allThesePersonsAreReturned(result, + "Peter, Smith", + "Rockshop e.K.", + "Ostfriesische Stahlhandel OHG", + "Erbengemeinschaft Bessler"); + } + + @Test + public void arbitraryUser_canViewOnlyItsOwnPerson() { + // given: + final var givenPerson = givenSomeTemporaryPerson("pac-admin-zzz00@zzz.example.com"); + + // when: + context("pac-admin-zzz00@zzz.example.com"); + final var result = personRepo.findPersonByOptionalNameLike(null); + + // then: + exactlyThesePersonsAreReturned(result, givenPerson.getTradeName()); + } + } + + @Nested + class FindByLabelLike { + + @Test + public void globalAdmin_withoutAssumedRole_canViewAllPersons() { + // given + context("alex@hostsharing.net", null); + + // when + final var result = personRepo.findPersonByOptionalNameLike("Rockshop"); + + // then + exactlyThesePersonsAreReturned(result, "Rockshop e.K."); + } + + @Test + public void arbitraryUser_withoutAssumedRole_canViewOnlyItsOwnPerson() { + // given: + final var givenPerson = givenSomeTemporaryPerson("drew@hostsharing.org"); + + // when: + context("drew@hostsharing.org"); + final var result = personRepo.findPersonByOptionalNameLike(givenPerson.getTradeName()); + + // then: + exactlyThesePersonsAreReturned(result, givenPerson.getTradeName()); + } + } + + @Nested + class DeleteByUuid { + + @Test + public void globalAdmin_withoutAssumedRole_canDeleteAnyPerson() { + // given + final var givenPerson = givenSomeTemporaryPerson("drew@hostsharing.org"); + + // when + final var result = jpaAttempt.transacted(() -> { + context("alex@hostsharing.net", null); + personRepo.deleteByUuid(givenPerson.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(jpaAttempt.transacted(() -> { + context("alex@hostsharing.net", null); + return personRepo.findPersonByOptionalNameLike(givenPerson.getTradeName()); + }).assertSuccessful().returnedValue()).hasSize(0); + } + + @Test + public void arbitraryUser_withoutAssumedRole_canDeleteAPersonCreatedByItself() { + // given + final var givenPerson = givenSomeTemporaryPerson("drew@hostsharing.org"); + + // when + final var result = jpaAttempt.transacted(() -> { + context("drew@hostsharing.org", null); + personRepo.deleteByUuid(givenPerson.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(jpaAttempt.transacted(() -> { + context("alex@hostsharing.net", null); + return personRepo.findPersonByOptionalNameLike(givenPerson.getTradeName()); + }).assertSuccessful().returnedValue()).hasSize(0); + } + } + + @AfterEach + void cleanup() { + context("alex@hostsharing.net", null); + final var result = personRepo.findPersonByOptionalNameLike("some temporary person"); + result.forEach(tempPerson -> { + System.out.println("DELETING person: " + tempPerson.getDisplayName()); + personRepo.deleteByUuid(tempPerson.getUuid()); + }); + } + + private HsAdminPersonEntity givenSomeTemporaryPerson( + final String createdByUser, + Supplier entitySupplier) { + return jpaAttempt.transacted(() -> { + context(createdByUser); + return personRepo.save(entitySupplier.get()); + }).assumeSuccessful().returnedValue(); + } + + private HsAdminPersonEntity givenSomeTemporaryPerson(final String createdByUser) { + return givenSomeTemporaryPerson(createdByUser, () -> + hsAdminPerson("some temporary person #" + RandomString.make(12))); + } + + void exactlyThesePersonsAreReturned(final List actualResult, final String... personLabels) { + assertThat(actualResult) + .extracting(HsAdminPersonEntity::getTradeName) + .containsExactlyInAnyOrder(personLabels); + } + + void allThesePersonsAreReturned(final List actualResult, final String... personLabels) { + assertThat(actualResult) + .extracting(HsAdminPersonEntity::getDisplayName) + .contains(personLabels); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/admin/person/TestHsAdminPerson.java b/src/test/java/net/hostsharing/hsadminng/hs/admin/person/TestHsAdminPerson.java new file mode 100644 index 00000000..c152e82e --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/admin/person/TestHsAdminPerson.java @@ -0,0 +1,16 @@ +package net.hostsharing.hsadminng.hs.admin.person; + +import java.util.UUID; + +public class TestHsAdminPerson { + + public static final HsAdminPersonEntity somePerson = hsAdminPerson("some person"); + + static public HsAdminPersonEntity hsAdminPerson(final String tradeName) { + return HsAdminPersonEntity.builder() + .uuid(UUID.randomUUID()) + .personType(HsAdminPersonType.NATURAL) + .tradeName(tradeName) + .build(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java index 67915331..1e4497ce 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java @@ -153,7 +153,9 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { hasEntry("granteeUserName", "pac-admin-yyy00@yyy.example.com") ) )) - .body("size()", is(1)); + .body("[0].grantedByRoleIdName", is("test_customer#yyy.admin")) + .body("[0].grantedRoleIdName", is("test_package#yyy00.admin")) + .body("[0].granteeUserName", is("pac-admin-yyy00@yyy.example.com")); // @formatter:on } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerAcceptanceTest.java index 4f8cecd1..bc465078 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerAcceptanceTest.java @@ -79,11 +79,20 @@ class RbacRoleControllerAcceptanceTest { .assertThat() .statusCode(200) .contentType("application/json") + .body("", hasItem(hasEntry("roleName", "test_customer#yyy.tenant"))) + .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa.owner"))) .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa.admin"))) + .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaab.owner"))) + .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaab.admin"))) .body("", hasItem(hasEntry("roleName", "test_package#yyy00.admin"))) .body("", hasItem(hasEntry("roleName", "test_package#yyy00.tenant"))) - .body("size()", is(7)); // increases with new test data + + .body("", not(hasItem(hasEntry("roleName", "test_customer#xxx.tenant")))) + .body("", not(hasItem(hasEntry("roleName", "test_domain#xxx00-aaaa.admin")))) + .body("", not(hasItem(hasEntry("roleName", "test_package#xxx00.admin")))) + .body("", not(hasItem(hasEntry("roleName", "test_package#xxx00.tenant")))) + ; // @formatter:on } @@ -101,11 +110,16 @@ class RbacRoleControllerAcceptanceTest { .then().assertThat() .statusCode(200) .contentType("application/json") + .body("", hasItem(hasEntry("roleName", "test_customer#zzz.tenant"))) .body("", hasItem(hasEntry("roleName", "test_domain#zzz00-aaaa.admin"))) .body("", hasItem(hasEntry("roleName", "test_package#zzz00.admin"))) .body("", hasItem(hasEntry("roleName", "test_package#zzz00.tenant"))) - .body("size()", is(7)); // increases with new test data + + .body("", not(hasItem(hasEntry("roleName", "test_customer#yyy.tenant")))) + .body("", not(hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa.admin")))) + .body("", not(hasItem(hasEntry("roleName", "test_package#yyy00.admin")))) + .body("", not(hasItem(hasEntry("roleName", "test_package#yyy00.tenant")))); // @formatter:on } } diff --git a/tools/generate b/tools/generate index cfc07e77..76f945f6 100755 --- a/tools/generate +++ b/tools/generate @@ -1,5 +1,16 @@ #!/bin/bash +mkdir -p src/test/java/net/hostsharing/hsadminng/hs/admin/person + +sed -e 's/hs-admin-contact/hs-admin-person/g' \ + -e 's/hs_admin_contact/hs_admin_person/g' \ + -e 's/HsAdminContact/HsAdminPerson/g' \ + -e 's/hsAdminContact/hsAdminPerson/g' \ + -e 's/contact/person/g' \ +src/test/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepositoryIntegrationTest.java + + sed -e 's/hs-admin-contact/hs-admin-person/g' \ -e 's/hs_admin_contact/hs_admin_person/g' \ src/main/resources/db/changelog/210-hs-admin-person.sql