move Parter+Debitor person+contact to related Relationsship #20

Merged
hsh-michaelhoennig merged 101 commits from remove-direct-partner-person-and-contact into master 2024-03-28 12:15:14 +01:00
13 changed files with 190 additions and 147 deletions
Showing only changes of commit 6a01002a05 - Show all commits

View File

@ -1,6 +1,6 @@
## *hsadmin-ng*'s Role-Based-Access-Management (RBAC) ## *hsadmin-ng*'s Role-Based-Access-Management (RBAC)
The requirements of *hsadmin-ng* include table-m row- and column-level-security for read and write access to business-objects. The requirements of *hsadmin-ng* option table-m row- and column-level-security for read and write access to business-objects.
More precisely, any access has to be controlled according to given rules depending on the accessing users, their roles and the accessed business-object. More precisely, any access has to be controlled according to given rules depending on the accessing users, their roles and the accessed business-object.
Further, roles and business-objects are hierarchical. Further, roles and business-objects are hierarchical.

View File

@ -110,9 +110,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
if (partnerRepo.deleteByUuid(partnerUuid) != 1 || if (partnerRepo.deleteByUuid(partnerUuid) != 1) {
// TODO: move to after delete trigger in partner
relationshipRepo.deleteByUuid(partnerToDelete.get().getPartnerRole().getUuid()) != 1 ) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
} }

View File

@ -20,7 +20,7 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatch
@Override @Override
public void apply(final HsOfficePartnerPatchResource resource) { public void apply(final HsOfficePartnerPatchResource resource) {
OptionalFromJson.of(resource.getPartnerRoleUuid()).ifPresent(newValue -> { OptionalFromJson.of(resource.getPartnerRoleUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact"); verifyNotNull(newValue, "partnerRole");
entity.setPartnerRole(em.getReference(HsOfficeRelationshipEntity.class, newValue)); entity.setPartnerRole(em.getReference(HsOfficeRelationshipEntity.class, newValue));
}); });

View File

@ -11,11 +11,13 @@ public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEnt
Optional<HsOfficePartnerEntity> findByUuid(UUID id); Optional<HsOfficePartnerEntity> findByUuid(UUID id);
List<HsOfficePartnerEntity> findAll(); // TODO: move to a repo in test sources
@Query(""" @Query("""
SELECT partner FROM HsOfficePartnerEntity partner SELECT partner FROM HsOfficePartnerEntity partner
JOIN HsOfficeRelationshipEntity rel ON rel.uuid = partner.partnerRole.uuid JOIN HsOfficeRelationshipEntity rel ON rel.uuid = partner.partnerRole.uuid
JOIN HsOfficeContactEntity contact ON contact.uuid = rel.contact.uuid JOIN HsOfficeContactEntity contact ON contact.uuid = rel.contact.uuid
JOIN HsOfficePersonEntity person ON person.uuid = rel.relAnchor.uuid JOIN HsOfficePersonEntity person ON person.uuid = rel.relHolder.uuid
WHERE :name is null WHERE :name is null
OR partner.details.birthName like concat(cast(:name as text), '%') OR partner.details.birthName like concat(cast(:name as text), '%')
OR contact.label like concat(cast(:name as text), '%') OR contact.label like concat(cast(:name as text), '%')

View File

@ -37,6 +37,7 @@ public class RbacGrantsMermaidService {
} }
public enum Include { public enum Include {
DETAILS,
USERS, USERS,
PERMISSIONS, PERMISSIONS,
NOT_ASSUMED, NOT_ASSUMED,
@ -53,84 +54,91 @@ public class RbacGrantsMermaidService {
@PersistenceContext @PersistenceContext
private EntityManager em; private EntityManager em;
public String allGrantsToCurrentUser(final EnumSet<Include> include) { public String allGrantsToCurrentUser(final EnumSet<Include> includes) {
final var graph = new HashSet<RawRbacGrantEntity>(); final var graph = new HashSet<RawRbacGrantEntity>();
for ( UUID subjectUuid: context.currentSubjectsUuids() ) { for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
traverseGrantsTo(graph, subjectUuid, include); traverseGrantsTo(graph, subjectUuid, includes);
} }
return toMermaidFlowchart(graph); return toMermaidFlowchart(graph, includes);
} }
private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> include) { private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> includes) {
final var grants = rawGrantRepo.findByAscendingUuid(refUuid); final var grants = rawGrantRepo.findByAscendingUuid(refUuid);
grants.forEach(g -> { grants.forEach(g -> {
if (!include.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) { if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) {
return; return;
} }
if (!include.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(" test_")) { if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(" test_")) {
return; return;
} }
if (!include.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(" test_")) { if (!includes.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(" test_")) {
return; return;
} }
graph.add(g); graph.add(g);
if (include.contains(NOT_ASSUMED) || g.isAssumed()) { if (includes.contains(NOT_ASSUMED) || g.isAssumed()) {
traverseGrantsTo(graph, g.getDescendantUuid(), include); traverseGrantsTo(graph, g.getDescendantUuid(), includes);
} }
}); });
} }
public String allGrantsFrom(final UUID targetObject, final String op, final EnumSet<Include> include) { public String allGrantsFrom(final UUID targetObject, final String op, final EnumSet<Include> includes) {
final var refUuid = (UUID) em.createNativeQuery("SELECT uuid FROM rbacpermission WHERE objectuuid=:targetObject AND op=:op") final var refUuid = (UUID) em.createNativeQuery("SELECT uuid FROM rbacpermission WHERE objectuuid=:targetObject AND op=:op")
.setParameter("targetObject", targetObject) .setParameter("targetObject", targetObject)
.setParameter("op", op) .setParameter("op", op)
.getSingleResult(); .getSingleResult();
final var graph = new HashSet<RawRbacGrantEntity>(); final var graph = new HashSet<RawRbacGrantEntity>();
traverseGrantsFrom(graph, refUuid, include); traverseGrantsFrom(graph, refUuid, includes);
return toMermaidFlowchart(graph); return toMermaidFlowchart(graph, includes);
} }
private void traverseGrantsFrom(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> include) { private void traverseGrantsFrom(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> option) {
final var grants = rawGrantRepo.findByDescendantUuid(refUuid); final var grants = rawGrantRepo.findByDescendantUuid(refUuid);
grants.forEach(g -> { grants.forEach(g -> {
if (!include.contains(USERS) && g.getAscendantIdName().startsWith("user ")) { if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user ")) {
return; return;
} }
graph.add(g); graph.add(g);
if (include.contains(NOT_ASSUMED) || g.isAssumed()) { if (option.contains(NOT_ASSUMED) || g.isAssumed()) {
traverseGrantsFrom(graph, g.getAscendingUuid(), include); traverseGrantsFrom(graph, g.getAscendingUuid(), option);
} }
}); });
} }
private String toMermaidFlowchart(final HashSet<RawRbacGrantEntity> graph) { private String toMermaidFlowchart(final HashSet<RawRbacGrantEntity> graph, final EnumSet<Include> includes) {
final var entities = graph.stream() final var entities =
includes.contains(DETAILS)
? graph.stream()
.flatMap(g -> Stream.of( .flatMap(g -> Stream.of(
new Node(g.getAscendantIdName(), g.getAscendingUuid()), new Node(g.getAscendantIdName(), g.getAscendingUuid()),
new Node(g.getDescendantIdName(), g.getDescendantUuid())) new Node(g.getDescendantIdName(), g.getDescendantUuid()))
) )
.collect(groupingBy(RbacGrantsMermaidService::entityIdName)); .collect(groupingBy(RbacGrantsMermaidService::renderEntityIdName))
.entrySet().stream()
return "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n" + .map(entity -> "subgraph " + quoted(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
"flowchart TB\n\n"
+ entities.entrySet().stream()
.map(entity -> "subgraph " + quoted(entity.getKey()) + subgraphDisplay(entity.getKey()) + "\n\n "
+ entity.getValue().stream() + entity.getValue().stream()
.map(n -> node(n.idName(), n.uuid()).replace("\n", "\n ")) .map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n "))
.sorted() .sorted()
.distinct() .distinct()
.collect(joining("\n\n "))) .collect(joining("\n\n ")))
.collect(joining("\n\nend\n\n")) .collect(joining("\n\nend\n\n"))
+ "\n\nend\n\n" + "\n\nend\n\n"
+ graph.stream() : "";
final var grants = graph.stream()
.map(g -> quoted(g.getAscendantIdName()) + .map(g -> quoted(g.getAscendantIdName()) +
(g.isAssumed() ? " --> " : " -.-> ") + (g.isAssumed() ? " --> " : " -.-> ") +
quoted(g.getDescendantIdName())) quoted(g.getDescendantIdName()))
.sorted() .sorted()
.collect(joining("\n")); .collect(joining("\n"));
final var avoidCroppedNodeLabels = "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n";
return (includes.contains(DETAILS) ? avoidCroppedNodeLabels : "")
+ "flowchart TB\n\n"
+ entities
+ grants;
} }
private String subgraphDisplay(final String entityId) { private String renderSubgraph(final String entityId) {
// this does not work according to Mermaid bug https://github.com/mermaid-js/mermaid/issues/3806 // this does not work according to Mermaid bug https://github.com/mermaid-js/mermaid/issues/3806
// if (entityId.contains("#")) { // if (entityId.contains("#")) {
// final var parts = entityId.split("#"); // final var parts = entityId.split("#");
@ -144,7 +152,7 @@ public class RbacGrantsMermaidService {
return "[" + entityId + "]"; return "[" + entityId + "]";
} }
private static String entityIdName(final Node node) { private static String renderEntityIdName(final Node node) {
final var refType = refType(node.idName()); final var refType = refType(node.idName());
if (refType.equals("user")) { if (refType.equals("user")) {
return "users"; return "users";
@ -159,11 +167,11 @@ public class RbacGrantsMermaidService {
throw new IllegalArgumentException("unknown refType '" + refType + "' in '" + node.idName() + "'"); throw new IllegalArgumentException("unknown refType '" + refType + "' in '" + node.idName() + "'");
} }
private String node(final String idName, final UUID uuid) { private String renderNode(final String idName, final UUID uuid) {
return quoted(idName) + nodeContent(idName, uuid); return quoted(idName) + renderNodeContent(idName, uuid);
} }
private String nodeContent(final String idName, final UUID uuid) { private String renderNodeContent(final String idName, final UUID uuid) {
final var refType = refType(idName); final var refType = refType(idName);
if (refType.equals("user")) { if (refType.equals("user")) {

View File

@ -50,7 +50,7 @@ components:
HsOfficePartnerPatch: HsOfficePartnerPatch:
type: object type: object
properties: properties:
partnerRoleUUid: partnerRoleUuid:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true

View File

@ -19,6 +19,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.EnumSet;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid;
@ -91,9 +92,9 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
void globalAdmin_withoutAssumedRole_canAddPartner() { void globalAdmin_withoutAssumedRole_canAddPartner() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0); final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").stream().findFirst().orElseThrow();
final var givenPerson = personRepo.findPersonByOptionalNameLike("Third").get(0); final var givenPerson = personRepo.findPersonByOptionalNameLike("Third").stream().findFirst().orElseThrow();
final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").get(0); final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").stream().findFirst().orElseThrow();
final var location = RestAssured // @formatter:off final var location = RestAssured // @formatter:off
.given() .given()
@ -107,8 +108,6 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
"relHolderUuid": "%s", "relHolderUuid": "%s",
"contactUuid": "%s" "contactUuid": "%s"
}, },
"personUuid": "%s",
"contactUuid": "%s",
"details": { "details": {
"registrationOffice": "Temp Registergericht Aurich", "registrationOffice": "Temp Registergericht Aurich",
"registrationNumber": "111111" "registrationNumber": "111111"
@ -117,21 +116,29 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
""".formatted( """.formatted(
givenMandantPerson.getUuid(), givenMandantPerson.getUuid(),
givenPerson.getUuid(), givenPerson.getUuid(),
givenContact.getUuid(),
givenPerson.getUuid(),
givenContact.getUuid())) givenContact.getUuid()))
.port(port) .port(port)
.when() .when()
.post("http://localhost/api/hs/office/partners") .post("http://localhost/api/hs/office/partners")
.then().assertThat() .then().log().body().assertThat()
.statusCode(201) .statusCode(201)
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body("uuid", isUuidValid()) .body("", lenientlyEquals("""
.body("partnerNumber", is(20002)) {
.body("details.registrationOffice", is("Temp Registergericht Aurich")) "partnerNumber": 20002,
.body("details.registrationNumber", is("111111")) "partnerRole": {
.body("contact.label", is(givenContact.getLabel())) "relAnchor": { "tradeName": "Hostsharing eG" },
.body("person.tradeName", is(givenPerson.getTradeName())) "relHolder": { "tradeName": "Third OHG" },
"relType": "PARTNER",
"relMark": null,
"contact": { "label": "fourth contact" }
},
"details": {
"registrationOffice": "Temp Registergericht Aurich",
"registrationNumber": "111111"
}
}
"""))
.header("Location", startsWith("http://localhost")) .header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on .extract().header("Location"); // @formatter:on
@ -226,6 +233,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
@Test @Test
void globalAdmin_withoutAssumedRole_canGetArbitraryPartner() { void globalAdmin_withoutAssumedRole_canGetArbitraryPartner() {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var partners = partnerRepo.findAll();
final var givenPartnerUuid = partnerRepo.findPartnerByOptionalNameLike("First").get(0).getUuid(); final var givenPartnerUuid = partnerRepo.findPartnerByOptionalNameLike("First").get(0).getUuid();
RestAssured // @formatter:off RestAssured // @formatter:off
@ -239,8 +247,18 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
.contentType("application/json") .contentType("application/json")
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"person": { "tradeName": "First GmbH" }, "partnerNumber": 10001,
"partnerRole": {
"relAnchor": { "tradeName": "Hostsharing eG" },
"relHolder": { "tradeName": "First GmbH" },
"relType": "PARTNER",
"contact": { "label": "first contact" } "contact": { "label": "first contact" }
},
"details": {
"registrationOffice": "Hamburg",
"registrationNumber": "RegNo123456789"
}
}
} }
""")); // @formatter:on """)); // @formatter:on
} }
@ -278,9 +296,11 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
.contentType("application/json") .contentType("application/json")
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"person": { "tradeName": "First GmbH" }, "partnerRole": {
"relHolder": { "tradeName": "First GmbH" },
"contact": { "label": "first contact" } "contact": { "label": "first contact" }
} }
}
""")); // @formatter:on """)); // @formatter:on
} }
} }
@ -295,8 +315,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenPartner = givenSomeTemporaryPartnerBessler(20011); final var givenPartner = givenSomeTemporaryPartnerBessler(20011);
final var givenPerson = personRepo.findPersonByOptionalNameLike("Third").get(0); final var givenPartnerRel = givenSomeTemporaryPartnerRel("Third OHG", "third contact");
final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").get(0);
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -305,8 +324,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body(""" .body("""
{ {
"partnerNumber": "20011", "partnerNumber": "20011",
"contactUuid": "%s", "partnerRoleUuid": "%s",
"personUuid": "%s",
"details": { "details": {
"registrationOffice": "Temp Registergericht Aurich", "registrationOffice": "Temp Registergericht Aurich",
"registrationNumber": "222222", "registrationNumber": "222222",
@ -315,18 +333,32 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
"dateOfDeath": "2022-01-12" "dateOfDeath": "2022-01-12"
} }
} }
""".formatted(givenContact.getUuid(), givenPerson.getUuid())) """.formatted(givenPartnerRel.getUuid()))
.port(port) .port(port)
.when() .when()
.patch("http://localhost/api/hs/office/partners/" + givenPartner.getUuid()) .patch("http://localhost/api/hs/office/partners/" + givenPartner.getUuid())
.then().assertThat() .then().log().body().assertThat()
.statusCode(200) .statusCode(200)
.contentType(ContentType.JSON) .contentType(ContentType.JSON)
.body("uuid", is(givenPartner.getUuid().toString())) // not patched! .body("", lenientlyEquals("""
.body("partnerNumber", is(givenPartner.getPartnerNumber())) // not patched! {
.body("details.registrationNumber", is("222222")) "partnerNumber": 20011,
.body("contact.label", is(givenContact.getLabel())) "partnerRole": {
.body("person.tradeName", is(givenPerson.getTradeName())); "relAnchor": { "tradeName": "Hostsharing eG" },
"relHolder": { "tradeName": "Third OHG" },
"relType": "PARTNER",
"contact": { "label": "third contact" }
},
"details": {
"registrationOffice": "Temp Registergericht Aurich",
"registrationNumber": "222222",
"birthName": "Maja Schmidt",
"birthPlace": null,
"birthday": "1938-04-08",
"dateOfDeath": "2022-01-12"
}
}
"""));
// @formatter:on // @formatter:on
// finally, the partner is actually updated // finally, the partner is actually updated
@ -334,9 +366,8 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isPresent().get() assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isPresent().get()
.matches(partner -> { .matches(partner -> {
assertThat(partner.getPartnerNumber()).isEqualTo(givenPartner.getPartnerNumber()); assertThat(partner.getPartnerNumber()).isEqualTo(givenPartner.getPartnerNumber());
// TODO: assert partnerRole assertThat(partner.getPartnerRole().getRelHolder().getTradeName()).isEqualTo("Third OHG");
// assertThat(partner.getPerson().getTradeName()).isEqualTo("Third OHG"); assertThat(partner.getPartnerRole().getContact().getLabel()).isEqualTo("third contact");
// assertThat(partner.getContact().getLabel()).isEqualTo("fourth contact");
assertThat(partner.getDetails().getRegistrationOffice()).isEqualTo("Temp Registergericht Aurich"); assertThat(partner.getDetails().getRegistrationOffice()).isEqualTo("Temp Registergericht Aurich");
assertThat(partner.getDetails().getRegistrationNumber()).isEqualTo("222222"); assertThat(partner.getDetails().getRegistrationNumber()).isEqualTo("222222");
assertThat(partner.getDetails().getBirthName()).isEqualTo("Maja Schmidt"); assertThat(partner.getDetails().getBirthName()).isEqualTo("Maja Schmidt");
@ -460,13 +491,14 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
} }
} }
private HsOfficePartnerEntity givenSomeTemporaryPartnerBessler(final Integer partnerNumber) { private HsOfficeRelationshipEntity givenSomeTemporaryPartnerRel(
final String partnerHolderName,
final String contactName) {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0); final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").stream().findFirst().orElseThrow();
final var givenPerson = personRepo.findPersonByOptionalNameLike(partnerHolderName).stream().findFirst().orElseThrow();
final var givenPerson = personRepo.findPersonByOptionalNameLike("Erben Bessler").get(0); final var givenContact = contactRepo.findContactByOptionalLabelLike(contactName).stream().findFirst().orElseThrow();
final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0);
final var partnerRole = new HsOfficeRelationshipEntity(); final var partnerRole = new HsOfficeRelationshipEntity();
partnerRole.setRelType(HsOfficeRelationshipType.PARTNER); partnerRole.setRelType(HsOfficeRelationshipType.PARTNER);
@ -474,6 +506,13 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
partnerRole.setRelHolder(givenPerson); partnerRole.setRelHolder(givenPerson);
partnerRole.setContact(givenContact); partnerRole.setContact(givenContact);
em.persist(partnerRole); em.persist(partnerRole);
return partnerRole;
}).assertSuccessful().returnedValue();
}
private HsOfficePartnerEntity givenSomeTemporaryPartnerBessler(final Integer partnerNumber) {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
final var partnerRole = em.merge(givenSomeTemporaryPartnerRel("Erben Bessler", "fourth contact"));
final var newPartner = HsOfficePartnerEntity.builder() final var newPartner = HsOfficePartnerEntity.builder()
.partnerRole(partnerRole) .partnerRole(partnerRole)

View File

@ -191,31 +191,5 @@ class HsOfficePartnerControllerRestTest {
// then // then
.andExpect(status().isForbidden()); .andExpect(status().isForbidden());
} }
@Test
void respondBadRequest_ifRelationshipCannotBeDeleted() throws Exception {
// given
final UUID givenPartnerUuid = UUID.randomUUID();
when(partnerRepo.findByUuid(givenPartnerUuid)).thenReturn(Optional.of(partnerMock));
when(partnerRepo.deleteByUuid(givenPartnerUuid)).thenReturn(1);
when(relationshipRepo.deleteByUuid(any())).thenReturn(0);
final UUID givenRelationshipUuid = UUID.randomUUID();
when(partnerMock.getPartnerRole()).thenReturn(HsOfficeRelationshipEntity.builder()
.uuid(givenRelationshipUuid)
.build());
when(relationshipRepo.deleteByUuid(givenRelationshipUuid)).thenReturn(0);
// when
mockMvc.perform(MockMvcRequestBuilders
.delete("/api/hs/office/partners/" + givenPartnerUuid)
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
// then
.andExpect(status().isForbidden());
}
} }
} }

View File

@ -48,10 +48,8 @@ class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase<
@BeforeEach @BeforeEach
void initMocks() { void initMocks() {
lenient().when(em.getReference(eq(HsOfficeContactEntity.class), any())).thenAnswer(invocation -> lenient().when(em.getReference(eq(HsOfficeRelationshipEntity.class), any())).thenAnswer(invocation ->
HsOfficeContactEntity.builder().uuid(invocation.getArgument(1)).build()); HsOfficeRelationshipEntity.builder().uuid(invocation.getArgument(1)).build());
lenient().when(em.getReference(eq(HsOfficePersonEntity.class), any())).thenAnswer(invocation ->
HsOfficePersonEntity.builder().uuid(invocation.getArgument(1)).build());
} }
@Override @Override

View File

@ -8,8 +8,7 @@ import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepo
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType;
import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsMermaidService; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacObjectRepository;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsMermaidService.Include;
import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
import net.hostsharing.test.Array; import net.hostsharing.test.Array;
import net.hostsharing.test.JpaAttempt; import net.hostsharing.test.JpaAttempt;
@ -26,19 +25,18 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import static java.lang.String.join;
import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacObjectEntity.objectDisplaysOf;
import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
import static net.hostsharing.test.Array.fromFormatted; import static net.hostsharing.test.Array.fromFormatted;
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;
@DataJpaTest @DataJpaTest
@Import( { Context.class, JpaAttempt.class, RbacGrantsMermaidService.class }) @Import( { Context.class, JpaAttempt.class })
class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithCleanup { class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
@Autowired @Autowired
@ -53,15 +51,15 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
@Autowired @Autowired
HsOfficeContactRepository contactRepo; HsOfficeContactRepository contactRepo;
@Autowired
RawRbacObjectRepository rawObjectRepo;
@Autowired @Autowired
RawRbacRoleRepository rawRoleRepo; RawRbacRoleRepository rawRoleRepo;
@Autowired @Autowired
RawRbacGrantRepository rawGrantRepo; RawRbacGrantRepository rawGrantRepo;
@Autowired
RbacGrantsMermaidService mermaidService;
@PersistenceContext @PersistenceContext
EntityManager em; EntityManager em;
@ -263,9 +261,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
givenPartner, givenPartner,
"hs_office_person#ErbenBesslerMelBessler.admin"); "hs_office_person#ErbenBesslerMelBessler.admin");
assertThatPartnerActuallyInDatabase(givenPartner); assertThatPartnerActuallyInDatabase(givenPartner);
RbacGrantsMermaidService.writeToFile("initial partner: Erben Bessler + fifth contact",
mermaidService.allGrantsFrom(givenPartner.getUuid(), "view", EnumSet.of(Include.USERS)),
"doc/all-grants-before-update.md");
// when // when
@ -277,9 +272,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
// then // then
result.assertSuccessful(); result.assertSuccessful();
RbacGrantsMermaidService.writeToFile("updated partner: Third OHG + sixth contact",
mermaidService.allGrantsFrom(result.returnedValue().getUuid(), "view", EnumSet.of(Include.USERS)),
"doc/all-grants-after-update.md");
assertThatPartnerIsVisibleForUserWithRole( assertThatPartnerIsVisibleForUserWithRole(
result.returnedValue(), result.returnedValue(),
"global#global.admin"); "global#global.admin");
@ -298,13 +290,13 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
final var givenPartner = givenSomeTemporaryHostsharingPartner(20037, "Erben Bessler", "ninth"); final var givenPartner = givenSomeTemporaryHostsharingPartner(20037, "Erben Bessler", "ninth");
assertThatPartnerIsVisibleForUserWithRole( assertThatPartnerIsVisibleForUserWithRole(
givenPartner, givenPartner,
"hs_office_partner#20033:ErbenBesslerMelBessler-ninthcontact.agent"); "hs_office_person#ErbenBesslerMelBessler.admin");
assertThatPartnerActuallyInDatabase(givenPartner); assertThatPartnerActuallyInDatabase(givenPartner);
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", context("superuser-alex@hostsharing.net",
"hs_office_partner#20033:ErbenBesslerMelBessler-ninthcontact.agent"); "hs_office_person#ErbenBesslerMelBessler.admin");
givenPartner.getDetails().setBirthName("new birthname"); givenPartner.getDetails().setBirthName("new birthname");
return partnerRepo.save(givenPartner); return partnerRepo.save(givenPartner);
}); });
@ -316,8 +308,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
private void assertThatPartnerActuallyInDatabase(final HsOfficePartnerEntity saved) { private void assertThatPartnerActuallyInDatabase(final HsOfficePartnerEntity saved) {
final var found = partnerRepo.findByUuid(saved.getUuid()); final var found = partnerRepo.findByUuid(saved.getUuid());
found.get().getPartnerRole(); // TODO: remove and uncomment the next line: assertThat(found).isNotEmpty().get().isNotSameAs(saved).usingRecursiveComparison().isEqualTo(saved);
// assertThat(found).isNotEmpty().get().isNotSameAs(saved).usingRecursiveComparison().isEqualTo(saved);
} }
private void assertThatPartnerIsVisibleForUserWithRole( private void assertThatPartnerIsVisibleForUserWithRole(
@ -334,10 +325,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
final String assumedRoles) { final String assumedRoles) {
jpaAttempt.transacted(() -> { jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", assumedRoles); context("superuser-alex@hostsharing.net", assumedRoles);
RbacGrantsMermaidService.writeToFile("partner visible in assertThatPartnerIsNotVisibleForUserWithRole",
mermaidService.allGrantsFrom(entity.getUuid(), "view", EnumSet.of(Include.USERS)),
"doc/all-grants-within-assertThatPartnerIsNotVisibleForUserWithRole.md");
final var found = partnerRepo.findByUuid(entity.getUuid()); final var found = partnerRepo.findByUuid(entity.getUuid());
assertThat(found).isEmpty(); assertThat(found).isEmpty();
}).assertSuccessful(); }).assertSuccessful();
@ -395,6 +382,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
public void deletingAPartnerAlsoDeletesRelatedRolesAndGrants() { public void deletingAPartnerAlsoDeletesRelatedRolesAndGrants() {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var initialObjects = Array.from(objectDisplaysOf(rawObjectRepo.findAll()));
final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll()));
final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll()));
final var givenPartner = givenSomeTemporaryHostsharingPartner(20034, "Erben Bessler", "twelfth"); final var givenPartner = givenSomeTemporaryHostsharingPartner(20034, "Erben Bessler", "twelfth");
@ -402,15 +390,13 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
// TODO: should deleting a partner automatically delete the PARTNER relationship? (same for debitor) return partnerRepo.deleteByUuid(givenPartner.getUuid());
// TODO: why did the test cleanup check does not notice this, if missing?
return partnerRepo.deleteByUuid(givenPartner.getUuid()) +
relationshipRepo.deleteByUuid(givenPartner.getPartnerRole().getUuid());
}); });
// then // then
result.assertSuccessful(); result.assertSuccessful();
assertThat(result.returnedValue()).isEqualTo(2); // partner+relationship assertThat(result.returnedValue()).isEqualTo(1);
assertThat(objectDisplaysOf(rawObjectRepo.findAll())).containsExactlyInAnyOrder(initialObjects);
assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames); assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames);
assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames);
} }
@ -439,7 +425,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var partnerRole = givenSomeTemporaryHostsharingPartnerRole(person, contact); final var partnerRole = givenSomeTemporaryHostsharingPartnerRole(person, contact);
// em.flush(); // TODO: why is that necessary?
final var newPartner = HsOfficePartnerEntity.builder() final var newPartner = HsOfficePartnerEntity.builder()
.partnerNumber(partnerNumber) .partnerNumber(partnerNumber)
@ -480,9 +465,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
@AfterEach @AfterEach
void cleanup() { void cleanup() {
cleanupAllNew(HsOfficePartnerDetailsEntity.class); // TODO: should not be necessary
cleanupAllNew(HsOfficePartnerEntity.class); cleanupAllNew(HsOfficePartnerEntity.class);
cleanupAllNew(HsOfficeRelationshipEntity.class);
} }
private String[] distinct(final String[] strings) { private String[] distinct(final String[] strings) {

View File

@ -4,7 +4,6 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsMermaidService.Include; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsMermaidService.Include;
import net.hostsharing.test.JpaAttempt; import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.Disabled;
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.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

View File

@ -0,0 +1,31 @@
package net.hostsharing.hsadminng.rbac.rbacrole;
import lombok.*;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.annotation.Immutable;
import jakarta.persistence.*;
import java.util.List;
import java.util.UUID;
@Entity
@Table(name = "rbacobject") // TODO: create view rbacobject_ev
@Getter
@Setter
@ToString
@Immutable
@NoArgsConstructor
@AllArgsConstructor
public class RawRbacObjectEntity {
@Id
private UUID uuid;
@Column(name="objecttable")
private String objectTable;
@NotNull
public static List<String> objectDisplaysOf(@NotNull final List<RawRbacObjectEntity> roles) {
return roles.stream().map(e -> e.objectTable+ "#" + e.uuid).sorted().toList();
}
}

View File

@ -0,0 +1,11 @@
package net.hostsharing.hsadminng.rbac.rbacrole;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.UUID;
public interface RawRbacObjectRepository extends Repository<RawRbacObjectEntity, UUID> {
List<RawRbacObjectEntity> findAll();
}