diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..1242f5db --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,51 @@ +pipeline { + agent { + dockerfile { + filename 'etc/jenkinsAgent.Dockerfile' + // additionalBuildArgs ... + args '--network=bridge --user root -v $PWD:$PWD -v /var/run/docker.sock:/var/run/docker.sock --group-add 984' + reuseNode true + } + } + + environment { + DOCKER_HOST = 'unix:///var/run/docker.sock' + HSADMINNG_POSTGRES_ADMIN_USERNAME = 'admin' + HSADMINNG_POSTGRES_RESTRICTED_USERNAME = 'restricted' + HSADMINNG_MIGRATION_DATA_PATH = 'migration' + } + + triggers { + pollSCM('H/1 * * * *') + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage ('Compile & Test') { + steps { + sh './gradlew clean check --no-daemon -x pitest -x dependencyCheckAnalyze' + } + } + } + + post { + always { + // archive test results + junit 'build/test-results/test/*.xml' + + // archive the JaCoCo coverage report in XML and HTML format + jacoco(execPattern: '**/jacoco.exec', + classPattern: 'build/classes/java/main', + sourcePattern: 'src/main/java', + exclusionPattern: '') + + // cleanup workspace + cleanWs() + } + } +} diff --git a/etc/jenkinsAgent.Dockerfile b/etc/jenkinsAgent.Dockerfile new file mode 100644 index 00000000..648e2f8e --- /dev/null +++ b/etc/jenkinsAgent.Dockerfile @@ -0,0 +1,10 @@ +FROM eclipse-temurin:21-jdk +RUN apt-get update && \ + apt-get install -y bind9-utils && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + + +# RUN mkdir /opt/app +# COPY japp.jar /opt +# CMD ["java", "-jar", "/opt/app/japp.jar"] diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java index 22a113f0..bb5c4914 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java @@ -37,7 +37,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { private HsOfficePersonRepository holderRepo; @Autowired - private HsOfficeContactRealRepository contactrealRepo; + private HsOfficeContactRealRepository realContactRepo; @PersistenceContext private EntityManager em; @@ -47,12 +47,20 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { public ResponseEntity> listRelations( final String currentSubject, final String assumedRoles, - final UUID personUuid, - final HsOfficeRelationTypeResource relationType) { + UUID personUuid, + HsOfficeRelationTypeResource relationType, + String personData, + String contactData) { context.define(currentSubject, assumedRoles); - final var entities = relationRbacRepo.findRelationRelatedToPersonUuidAndRelationType(personUuid, - relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name())); + final var entities = + ( personData == null && contactData == null ) + ? relationRbacRepo.findRelationRelatedToPersonUuidAndRelationType(personUuid, + relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name())) + : relationRbacRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData( + personUuid, + relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()), + forLike(personData), forLike(contactData)); final var resources = mapper.mapList(entities, HsOfficeRelationResource.class, RELATION_ENTITY_TO_RESOURCE_POSTMAPPER); @@ -77,7 +85,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow( () -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid()) )); - entityToSave.setContact(contactrealRepo.findByUuid(body.getContactUuid()).orElseThrow( + entityToSave.setContact(realContactRepo.findByUuid(body.getContactUuid()).orElseThrow( () -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid()) )); @@ -144,6 +152,9 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi { return ResponseEntity.ok(mapped); } + private String forLike(final String text) { + return text == null ? null : ("%" + text.toLowerCase() + "%"); + } final BiConsumer RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class)); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java index ec9aea59..4b83de46 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRbacRepository.java @@ -29,6 +29,36 @@ public interface HsOfficeRelationRbacRepository extends Repository findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType); + /** + * Finds relations by a conjunction of optional criteria, including anchorPerson, holderPerson and contact data. + * + * @param personUuid the optional UUID of the anchorPerson or holderPerson + * @param relationType the type of the relation + * @param personData a lower-case string to match the persons tradeName, familyName or givenName (use '%' for wildcard) + * @param contactData a lower-case string to match the contacts caption, postalAddress, emailAddresses or phoneNumbers (use '%' for wildcard) + * @return a list of (accessible) relations which match all given criteria + */ + @Query(value = """ + SELECT rel FROM HsOfficeRelationRbacEntity AS rel + WHERE (:relationType IS NULL OR CAST(rel.type AS String) = CAST(:relationType AS String)) + AND ( :personUuid IS NULL OR + rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid ) + AND ( :personData IS NULL OR + lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData OR + lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData OR + lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData ) + AND ( :contactData IS NULL OR + lower(rel.contact.caption) LIKE :contactData OR + lower(rel.contact.postalAddress) LIKE :contactData OR + lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData OR + lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData ) + """) + List findRelationRelatedToPersonUuidRelationTypePersonAndContactData( + UUID personUuid, + HsOfficeRelationType relationType, + String personData, + String contactData); + HsOfficeRelationRbacEntity save(final HsOfficeRelationRbacEntity entity); long count(); diff --git a/src/main/resources/api-definition/hs-office/hs-office-relations.yaml b/src/main/resources/api-definition/hs-office/hs-office-relations.yaml index ce7a865b..77d9dda0 100644 --- a/src/main/resources/api-definition/hs-office/hs-office-relations.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office-relations.yaml @@ -1,6 +1,8 @@ get: summary: Returns a list of (optionally filtered) person relations for a given person. - description: Returns the list of (optionally filtered) person relations of a given person and which are visible to the current subject or any of it's assumed roles. + description: + Returns the list of (optionally filtered) person relations of a given person and which are visible to the current subject or any of it's assumed roles. + To match data, all given query parameters must be fulfilled ('and' / logical conjunction). tags: - hs-office-relations operationId: listRelations @@ -9,7 +11,7 @@ get: - $ref: 'auth.yaml#/components/parameters/assumedRoles' - name: personUuid in: query - required: true + required: false schema: type: string format: uuid @@ -20,6 +22,18 @@ get: schema: $ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType' description: Prefix of name properties from holder or contact to filter the results. + - name: personData + in: query + required: false + schema: + type: string + description: 'Data from any of these text field in the anchor or holder person: tradeName, familyName, givenName' + - name: contactData + in: query + required: false + schema: + type: string + description: 'Data from any of these text field in the contact: caption, postalAddress, emailAddresses, phoneNumbers' responses: "200": description: OK diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java index 41684c3b..607485bf 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java @@ -176,6 +176,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { // given final var givenEntity = validEntityBuilder().build(); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + Dns.fakeResultForDomain(givenEntity.getIdentifier(), Dns.Result.fromRecords()); // when final var errors = validator.validateContext(givenEntity); @@ -317,6 +318,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { )) .build(); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // when final var errors = validator.validateContext(givenEntity); @@ -340,6 +342,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { )) .build(); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // when final var zonefileErrors = new ArrayList(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java index 23e8410b..405bee93 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java @@ -9,7 +9,6 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationTypeResource; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; -import org.json.JSONException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -55,7 +54,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean class ListRelations { @Test - void globalAdmin_withoutAssumedRoles_canViewAllRelationsOfGivenPersonAndType() throws JSONException { + void globalAdmin_withoutAssumedRoles_canViewAllRelationsOfGivenPersonAndType() { // given context.define("superuser-alex@hostsharing.net"); @@ -113,7 +112,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean } @Test - void personAdmin_canViewAllRelationsOfGivenRelatedPersonAndAnyType() throws JSONException { + void personAdmin_canViewAllRelationsOfGivenRelatedPersonAndAnyType() { // given context.define("contact-admin@firstcontact.example.com"); @@ -125,7 +124,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean .port(port) .when() .get("http://localhost/api/hs/office/relations?personUuid=%s" - .formatted(givenPerson.getUuid(), HsOfficeRelationTypeResource.PARTNER)) + .formatted(givenPerson.getUuid())) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") @@ -169,6 +168,50 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean """)); // @formatter:on } + + @Test + void globalAdmin_canViewAllRelationsWithGivenContactData() { + + // given + context.define("superuser-alex@hostsharing.net"); + + RestAssured // @formatter:off + .given() + .header("current-subject", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/office/relations?personData=firby&contactData=Contact-Admin@FirstContact.Example.COM") + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "anchor": { + "personType": "LEGAL_PERSON", + "tradeName": "First GmbH" + }, + "holder": { + "personType": "NATURAL_PERSON", + "givenName": "Susan", + "familyName": "Firby" + }, + "type": "REPRESENTATIVE", + "contact": { + "caption": "first contact", + "postalAddress": "Vorname Nachname\\nStraße Hnr\\nPLZ Stadt", + "emailAddresses": { + "main": "contact-admin@firstcontact.example.com" + }, + "phoneNumbers": { + "phone_office": "+49 123 1234567" + } + } + } + ] + """)); + // @formatter:on + } } @Nested diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 923c62e9..8cbd4b43 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -33,3 +33,10 @@ logging: level: liquibase: WARN net.ttddyy.dsproxy.listener: DEBUG # HOWTO: log meaningful SQL statements + org.testcontainers: DEBUG + com.github.dockerjava: DEBUG + +testcontainers: + network: + mode: host +