diff --git a/.dockerignore b/.dockerignore index 1dff951bc..5ae5188a0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,9 @@ +.github .idea -src +LICENSE +README.ms +docs target/** !target/fdp-spring-boot.jar !target/classes/application-production.yml +nb-configuration.xml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..626dd882e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "daily" + target-branch: "develop" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 97ce690fd..1c8a6ef68 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,9 +14,9 @@ jobs: PRIVATE_IMAGE: ${{ secrets.PRIVATE_REGISTRY_URL }}/fairdatapoint TAG_DEVELOP: develop TAG_LATEST: latest - JDK_VERSION: 11 - JDK_FILE: openjdk-14.0.1_linux-x64_bin.tar.gz - JDK_URL: https://download.java.net/java/GA/jdk14.0.1/664493ef4a6946b186ff29eb326336a2/7/GPL/openjdk-14.0.1_linux-x64_bin.tar.gz + JDK_VERSION: 15 + JDK_FILE: openjdk-15.0.2_linux-x64_bin.tar.gz + JDK_URL: https://download.java.net/java/GA/jdk15.0.2/0d1cfde4252546c6931946de8db48ee2/7/GPL/openjdk-15.0.2_linux-x64_bin.tar.gz services: mongo: @@ -48,7 +48,7 @@ jobs: key: ${{ env.JDK_FILE }} # (2) -> Prepare Java - - name: Download Oracle JDK + - name: Download JDK run: | if [ ! -f ~/jdk/$JDK_FILE ]; then wget --quiet $JDK_URL -O ~/jdk/$JDK_FILE @@ -56,9 +56,9 @@ jobs: cp ~/jdk/$JDK_FILE . - name: Setup Java - uses: actions/setup-java@master + uses: actions/setup-java@v1 with: - version: ${{ env.JDK_VERSION }} + java-version: ${{ env.JDK_VERSION }} jdkFile: ${{ env.JDK_FILE }} - name: Verify Maven and Java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea850b0e..80770f2ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,29 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres +to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [1.8.0] + +### Added + +- Denylist for FDP Index pings +- Endpoints for managing Index settings from [Client] + +### Changed + +- Upgrade Java JDK from 14 to 15 +- Rate limits use forwarded IP by proxy based on config +- Index settings are moved to the database +- Admin trigger now accepts the same DTO as ping + +### Fixed + +- Fix metadata not found error + ## [1.7.0] ### Added @@ -155,4 +173,7 @@ The first release of reference FAIR Data Point implementation. [1.4.0]: /../../tree/v1.4.0 [1.5.0]: /../../tree/v1.5.0 [1.6.0]: /../../tree/v1.6.0 + [1.7.0]: /../../tree/v1.7.0 + +[1.8.0]: /../../tree/v1.8.0 diff --git a/Dockerfile b/Dockerfile index 7f4f71564..863d920fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ # THE SOFTWARE. # -FROM openjdk:14-jdk-slim +FROM openjdk:15-jdk-slim WORKDIR /fdp diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 000000000..5b58330de --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,42 @@ +# +# The MIT License +# Copyright © 2017 DTL +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +################################################################################ +# BUILD STAGE +FROM maven:3-openjdk-15 as builder + +WORKDIR /builder + +ADD . /builder + +RUN mvn --quiet -B -U --fail-fast -DskipTests package + +################################################################################ +# RUN STAGE +FROM openjdk:15-jdk-slim + +WORKDIR /fdp + +COPY --from=builder /builder/target/fdp-spring-boot.jar /fdp/app.jar +COPY --from=builder /builder/target/classes/application-production.yml /fdp/application.yml + +ENTRYPOINT java -jar app.jar --spring.profiles.active=production --spring.config.location=classpath:/application.yml,classpath:/application-production.yml,file:/fdp/application.yml diff --git a/README.md b/README.md index d8349e5a3..338e0556b 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ More information about FDP and how to deploy can be found at [FDP Deployment Doc **Stack:** - - **Java** (minimally JDK 14, or higher) - - **Maven** (recommended 3.2.5 or higher) - - **Docker** (recommended 17.09.0-ce or higher) - *for build of production image* + - **Java** (minimal: JDK 15) + - **Maven** (recommended: 3.2.5 or higher) + - **Docker** (recommended: 17.09.0-ce or higher) - *for build of production image* ### Build & Run -To run the application, a mongodb instance is required to be running. To configure the mongodb address, instruct spring-boot to use the `development` profile. Run these commands from the root of the project. +To run the application, a MongoDB instance is required to be running. To configure the mongodb address, instruct spring-boot to use the `development` profile. Run these commands from the root of the project. ```bash $ mvn spring-boot:run -Dspring-boot.run.profiles=development @@ -40,7 +40,7 @@ $ mvn spring-boot:run ### Run tests -Run these commands from the root of the project +Run these commands from the root of the project: ```bash $ mvn test @@ -48,7 +48,7 @@ $ mvn test ### Package the application -Run these commands from the root of the project +Run these commands from the root of the project: ```bash $ mvn package @@ -56,10 +56,18 @@ $ mvn package ### Create a Docker image -Run these commands from the root of the project +Run these commands from the root of the project (requires building jar file using `mvn package`): ```bash -$ docker build -t fairdata/fairdatapoint . +$ docker build -t fairdatapoint:local . +``` + +### Build using Docker + +If you do not have Java and Maven locally, you can build the Docker image using Docker (instead of using locally built jar file): + +```bash +$ docker build -f Dockefile.build -t fairdatapoint:local . ``` ## Security diff --git a/pom.xml b/pom.xml index 42b3f16cc..2fd5585fb 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ nl.dtls fairdatapoint - 1.7.0 + 1.8.0 jar FairDataPoint @@ -41,8 +41,8 @@ UTF-8 - 14 - 14 + 15 + 15 1.1.0.RELEASE @@ -54,15 +54,15 @@ 1.2.3 3.0.0 1.4.9 - 0.10.5 - 1.18.10 + 0.11.2 + 1.18.18 3.0 0.7.6.201602180812 4.3.0 2.3.1 - 2.2.4 + 4.0.3 0.2.0 @@ -315,6 +315,21 @@ false + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*.java + + + **/*Fixtures.java + **/*Config.java + **/Common.java + **/common/*.java + + + com.github.kburger rdf4j-generator-maven-plugin diff --git a/src/main/java/nl/dtls/fairdatapoint/api/controller/index/AdminController.java b/src/main/java/nl/dtls/fairdatapoint/api/controller/index/AdminController.java index ebbe66b6c..ad4886a65 100644 --- a/src/main/java/nl/dtls/fairdatapoint/api/controller/index/AdminController.java +++ b/src/main/java/nl/dtls/fairdatapoint/api/controller/index/AdminController.java @@ -24,7 +24,9 @@ import io.swagger.annotations.ApiOperation; import lombok.extern.log4j.Log4j2; +import nl.dtls.fairdatapoint.api.dto.index.ping.PingDTO; import nl.dtls.fairdatapoint.entity.index.event.Event; +import nl.dtls.fairdatapoint.service.UtilityService; import nl.dtls.fairdatapoint.service.index.event.EventService; import nl.dtls.fairdatapoint.service.index.webhook.WebhookService; import org.springframework.beans.factory.annotation.Autowired; @@ -33,6 +35,7 @@ import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; import java.util.UUID; @Log4j2 @@ -40,6 +43,9 @@ @RequestMapping("/index/admin") public class AdminController { + @Autowired + private UtilityService utilityService; + @Autowired private EventService eventService; @@ -50,9 +56,20 @@ public class AdminController { @PostMapping("/trigger") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) - public void triggerMetadataRetrieve(@RequestParam(required = false) String clientUrl, HttpServletRequest request) { - log.info("Received ping from {}", request.getRemoteAddr()); - final Event event = eventService.acceptAdminTrigger(request, clientUrl); + public void triggerMetadataRetrieve(@RequestBody @Valid PingDTO reqDto, HttpServletRequest request) { + log.info("Received ping from {}", utilityService.getRemoteAddr(request)); + final Event event = eventService.acceptAdminTrigger(request, reqDto); + webhookService.triggerWebhooks(event); + eventService.triggerMetadataRetrieval(event); + } + + @ApiOperation(value = "trigger-all", hidden = true) + @PostMapping("/trigger-all") + @PreAuthorize("hasRole('ADMIN')") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void triggerMetadataRetrieveAll(HttpServletRequest request) { + log.info("Received ping from {}", utilityService.getRemoteAddr(request)); + final Event event = eventService.acceptAdminTriggerAll(request); webhookService.triggerWebhooks(event); eventService.triggerMetadataRetrieval(event); } @@ -62,7 +79,7 @@ public void triggerMetadataRetrieve(@RequestParam(required = false) String clien @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) public void webhookPing(@RequestParam(required = true) UUID webhook, HttpServletRequest request) { - log.info("Received webhook {} ping trigger from {}", webhook, request.getRemoteAddr()); + log.info("Received webhook {} ping trigger from {}", webhook, utilityService.getRemoteAddr(request)); final Event event = webhookService.handleWebhookPing(request, webhook); webhookService.triggerWebhooks(event); } diff --git a/src/main/java/nl/dtls/fairdatapoint/api/controller/index/IndexEntryController.java b/src/main/java/nl/dtls/fairdatapoint/api/controller/index/IndexEntryController.java index 55ea3ac84..555943054 100644 --- a/src/main/java/nl/dtls/fairdatapoint/api/controller/index/IndexEntryController.java +++ b/src/main/java/nl/dtls/fairdatapoint/api/controller/index/IndexEntryController.java @@ -25,18 +25,15 @@ import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryDTO; import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryDetailDTO; import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryInfoDTO; -import nl.dtls.fairdatapoint.service.index.entry.IndexEntryMapper; import nl.dtls.fairdatapoint.service.index.entry.IndexEntryService; -import nl.dtls.fairdatapoint.service.index.event.EventService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; @RestController @RequestMapping("/index/entries") @@ -45,27 +42,26 @@ public class IndexEntryController { @Autowired private IndexEntryService service; - @Autowired - private EventService eventService; - - @Autowired - private IndexEntryMapper mapper; - @GetMapping("") public Page getEntriesPage(Pageable pageable, @RequestParam(required = false, defaultValue = "") String state) { - return service.getEntriesPage(pageable, state).map(mapper::toDTO); + return service.getEntriesPageDTOs(pageable, state); } @RequestMapping(value = "/{uuid}", method = RequestMethod.GET) public Optional getEntry(@PathVariable final String uuid) { - return service.getEntry(uuid).map(entry -> mapper.toDetailDTO(entry, - eventService.getEvents(entry.getUuid()))); + return service.getEntryDetailDTO(uuid); + } + + @RequestMapping(value = "/{uuid}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteEntry(@PathVariable final String uuid) { + service.deleteEntry(uuid); } @GetMapping("/all") public List getEntriesAll() { - return StreamSupport.stream(service.getAllEntries().spliterator(), true).map(mapper::toDTO).collect(Collectors.toList()); + return service.getAllEntriesAsDTOs(); } @GetMapping("/info") diff --git a/src/main/java/nl/dtls/fairdatapoint/api/controller/index/PingController.java b/src/main/java/nl/dtls/fairdatapoint/api/controller/index/PingController.java index 9337d12de..51aa5afea 100644 --- a/src/main/java/nl/dtls/fairdatapoint/api/controller/index/PingController.java +++ b/src/main/java/nl/dtls/fairdatapoint/api/controller/index/PingController.java @@ -26,6 +26,7 @@ import nl.dtls.fairdatapoint.api.dto.index.ping.PingDTO; import nl.dtls.fairdatapoint.database.rdf.repository.exception.MetadataRepositoryException; import nl.dtls.fairdatapoint.entity.index.event.Event; +import nl.dtls.fairdatapoint.service.UtilityService; import nl.dtls.fairdatapoint.service.index.event.EventService; import nl.dtls.fairdatapoint.service.index.harvester.HarvesterService; import nl.dtls.fairdatapoint.service.index.webhook.WebhookService; @@ -33,6 +34,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; @@ -51,6 +53,9 @@ public class PingController { @Autowired private HarvesterService harvesterService; + + @Autowired + private UtilityService utilityService; @ApiOperation( value = "Ping payload with FAIR Data Point info", @@ -59,12 +64,13 @@ public class PingController { ) @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") @ResponseStatus(HttpStatus.NO_CONTENT) - public void receivePing(@RequestBody @Valid PingDTO reqDto, HttpServletRequest request) throws MetadataRepositoryException { - logger.info("Received ping from {}", request.getRemoteAddr()); + public ResponseEntity receivePing(@RequestBody @Valid PingDTO reqDto, HttpServletRequest request) throws MetadataRepositoryException { + logger.info("Received ping from {}", utilityService.getRemoteAddr(request)); final Event event = eventService.acceptIncomingPing(reqDto, request); logger.info("Triggering metadata retrieval for {}", event.getRelatedTo().getClientUrl()); eventService.triggerMetadataRetrieval(event); harvesterService.harvest(reqDto.getClientUrl()); webhookService.triggerWebhooks(event); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/nl/dtls/fairdatapoint/api/controller/index/SettingsController.java b/src/main/java/nl/dtls/fairdatapoint/api/controller/index/SettingsController.java new file mode 100644 index 000000000..1086ae6dd --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/api/controller/index/SettingsController.java @@ -0,0 +1,61 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.api.controller.index; + +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsUpdateDTO; +import nl.dtls.fairdatapoint.service.index.settings.IndexSettingsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/index/settings") +public class SettingsController { + + @Autowired + private IndexSettingsService indexSettingsService; + + @RequestMapping(method = RequestMethod.GET) + @PreAuthorize("hasRole('ADMIN')") + public IndexSettingsDTO getIndexSettings() { + return indexSettingsService.getCurrentSettings(); + } + + @RequestMapping(method = RequestMethod.PUT) + @PreAuthorize("hasRole('ADMIN')") + public IndexSettingsDTO updateIndexSettings(@RequestBody @Valid IndexSettingsUpdateDTO reqDto) { + return indexSettingsService.updateSettings(reqDto); + } + + @RequestMapping(method = RequestMethod.DELETE) + @PreAuthorize("hasRole('ADMIN')") + public IndexSettingsDTO resetIndexSettings() { + return indexSettingsService.resetSettings(); + } +} diff --git a/src/main/java/nl/dtls/fairdatapoint/api/controller/metadata/GenericMetaController.java b/src/main/java/nl/dtls/fairdatapoint/api/controller/metadata/GenericMetaController.java index bcde97da9..332fa18b9 100644 --- a/src/main/java/nl/dtls/fairdatapoint/api/controller/metadata/GenericMetaController.java +++ b/src/main/java/nl/dtls/fairdatapoint/api/controller/metadata/GenericMetaController.java @@ -91,7 +91,7 @@ public MetaDTO getMeta(HttpServletRequest request) throws MetadataServiceExcepti MemberDTO member = oMember.orElse(new MemberDTO(null, null)); // 5. Get state - MetaStateDTO state = metadataStateService.getState(model, rd); + MetaStateDTO state = metadataStateService.getState(entityUri, model, rd); return new MetaDTO(member, state); } diff --git a/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsDTO.java b/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsDTO.java new file mode 100644 index 000000000..448d4969d --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsDTO.java @@ -0,0 +1,45 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.api.dto.index.settings; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.validation.constraints.NotNull; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class IndexSettingsDTO { + @NotNull + private IndexSettingsRetrievalDTO retrieval; + + @NotNull + private IndexSettingsPingDTO ping; + + @NotNull + private Boolean isDefault; +} diff --git a/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsPingDTO.java b/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsPingDTO.java new file mode 100644 index 000000000..616e6826c --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsPingDTO.java @@ -0,0 +1,49 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.api.dto.index.settings; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import nl.dtls.fairdatapoint.api.validator.ValidDuration; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class IndexSettingsPingDTO { + @NotNull + @ValidDuration + private String validDuration; + @NotNull + @ValidDuration + private String rateLimitDuration; + @NotNull + private Integer rateLimitHits; + @NotNull + private List denyList; +} diff --git a/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsRetrievalDTO.java b/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsRetrievalDTO.java new file mode 100644 index 000000000..d630da343 --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsRetrievalDTO.java @@ -0,0 +1,45 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.api.dto.index.settings; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import nl.dtls.fairdatapoint.api.validator.ValidDuration; + +import javax.validation.constraints.NotNull; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class IndexSettingsRetrievalDTO { + @NotNull + @ValidDuration + private String rateLimitWait; + + @NotNull + @ValidDuration + private String timeout; +} diff --git a/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsUpdateDTO.java b/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsUpdateDTO.java new file mode 100644 index 000000000..09905c30f --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/api/dto/index/settings/IndexSettingsUpdateDTO.java @@ -0,0 +1,45 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.api.dto.index.settings; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class IndexSettingsUpdateDTO { + @Valid + @NotNull + private IndexSettingsRetrievalDTO retrieval; + + @Valid + @NotNull + private IndexSettingsPingDTO ping; +} diff --git a/src/main/java/nl/dtls/fairdatapoint/api/filter/LoggingFilter.java b/src/main/java/nl/dtls/fairdatapoint/api/filter/LoggingFilter.java index 28801d60c..2840b7109 100644 --- a/src/main/java/nl/dtls/fairdatapoint/api/filter/LoggingFilter.java +++ b/src/main/java/nl/dtls/fairdatapoint/api/filter/LoggingFilter.java @@ -27,7 +27,9 @@ */ package nl.dtls.fairdatapoint.api.filter; +import nl.dtls.fairdatapoint.service.UtilityService; import org.apache.logging.log4j.ThreadContext; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -41,12 +43,15 @@ @Component public class LoggingFilter extends OncePerRequestFilter { + @Autowired + private UtilityService utilityService; + @Override public void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain fc) throws IOException, ServletException { - ThreadContext.put("ipAddress", request.getRemoteAddr()); + ThreadContext.put("ipAddress", utilityService.getRemoteAddr(request)); ThreadContext.put("responseStatus", String.valueOf(response.getStatus())); ThreadContext.put("requestMethod", request.getMethod()); ThreadContext.put("requestURI", request.getRequestURI()); diff --git a/src/main/java/nl/dtls/fairdatapoint/api/validator/DurationValidator.java b/src/main/java/nl/dtls/fairdatapoint/api/validator/DurationValidator.java new file mode 100644 index 000000000..618dbd846 --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/api/validator/DurationValidator.java @@ -0,0 +1,47 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.api.validator; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import java.time.Duration; + +import static nl.dtls.fairdatapoint.util.ValueFactoryHelper.i; + +public class DurationValidator implements ConstraintValidator { + + @Override + public void initialize(ValidDuration text) { + } + + @Override + public boolean isValid(String text, ConstraintValidatorContext cxt) { + try { + Duration.parse(text); + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/nl/dtls/fairdatapoint/api/validator/ValidDuration.java b/src/main/java/nl/dtls/fairdatapoint/api/validator/ValidDuration.java new file mode 100644 index 000000000..f46b8ac4e --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/api/validator/ValidDuration.java @@ -0,0 +1,40 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.api.validator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = DurationValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidDuration { + + String message() default "Invalid ISO-8601 duration"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/nl/dtls/fairdatapoint/config/IndexConfig.java b/src/main/java/nl/dtls/fairdatapoint/config/IndexConfig.java deleted file mode 100644 index 7a4e43c79..000000000 --- a/src/main/java/nl/dtls/fairdatapoint/config/IndexConfig.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * The MIT License - * Copyright © 2017 DTL - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package nl.dtls.fairdatapoint.config; - -import nl.dtls.fairdatapoint.entity.index.config.EventsConfig; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; - -import java.time.Duration; - -@Configuration -public class IndexConfig { - - @Bean - @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) - public EventsConfig eventsConfig( - @Value("${fdp-index.events.retrieval.rateLimitWait:PT10M}") String cfgRetrievalRateLimitWait, - @Value("${fdp-index.events.retrieval.timeout:PT1M}") String cfgRetrievalTimeout, - @Value("${fdp-index.events.ping.validDuration:P7D}") String cfgPingValidDuration, - @Value("${fdp-index.events.ping.rateLimitDuration:PT6H}") String cfgPingRateLimitDuration, - @Value("${fdp-index.events.ping.rateLimitHits:10}") int cfgPingRateLimitHits - ) { - return EventsConfig.builder() - .retrievalRateLimitWait(Duration.parse(cfgRetrievalRateLimitWait)) - .retrievalTimeout(Duration.parse(cfgRetrievalTimeout)) - .pingValidDuration(Duration.parse(cfgPingValidDuration)) - .pingRateLimitDuration(Duration.parse(cfgPingRateLimitDuration)) - .pingRateLimitHits(cfgPingRateLimitHits) - .build(); - } - -} diff --git a/src/main/java/nl/dtls/fairdatapoint/config/SwaggerConfig.java b/src/main/java/nl/dtls/fairdatapoint/config/SwaggerConfig.java index 159c72a68..7db81a37b 100644 --- a/src/main/java/nl/dtls/fairdatapoint/config/SwaggerConfig.java +++ b/src/main/java/nl/dtls/fairdatapoint/config/SwaggerConfig.java @@ -71,7 +71,7 @@ private ApiInfo apiInfo() { " FAIR Data Point Specification" + " " + "
", - "1.7.0", + "1.8.0", "ATO", new Contact("Luiz Bonino", "https://github.com/FAIRDataTeam/FAIRDataPoint", diff --git a/src/main/java/nl/dtls/fairdatapoint/config/WebConfig.java b/src/main/java/nl/dtls/fairdatapoint/config/WebConfig.java index fb1c06d73..865b915aa 100644 --- a/src/main/java/nl/dtls/fairdatapoint/config/WebConfig.java +++ b/src/main/java/nl/dtls/fairdatapoint/config/WebConfig.java @@ -27,6 +27,7 @@ import nl.dtls.fairdatapoint.api.converter.ErrorConverter; import nl.dtls.fairdatapoint.api.converter.RdfConverter; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -88,5 +89,4 @@ public ObjectMapper objectMapper() { mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); return mapper; } - } diff --git a/src/main/java/nl/dtls/fairdatapoint/database/mongo/repository/IndexSettingsRepository.java b/src/main/java/nl/dtls/fairdatapoint/database/mongo/repository/IndexSettingsRepository.java new file mode 100644 index 000000000..01b672fb3 --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/database/mongo/repository/IndexSettingsRepository.java @@ -0,0 +1,33 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.database.mongo.repository; + +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettings; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +public interface IndexSettingsRepository extends MongoRepository { + Optional findFirstBy(); +} diff --git a/src/main/java/nl/dtls/fairdatapoint/entity/index/config/EventsConfig.java b/src/main/java/nl/dtls/fairdatapoint/entity/index/exception/PingDeniedException.java similarity index 74% rename from src/main/java/nl/dtls/fairdatapoint/entity/index/config/EventsConfig.java rename to src/main/java/nl/dtls/fairdatapoint/entity/index/exception/PingDeniedException.java index d42ef5eca..3fd6f1d0e 100644 --- a/src/main/java/nl/dtls/fairdatapoint/entity/index/config/EventsConfig.java +++ b/src/main/java/nl/dtls/fairdatapoint/entity/index/exception/PingDeniedException.java @@ -20,19 +20,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package nl.dtls.fairdatapoint.entity.index.config; +package nl.dtls.fairdatapoint.entity.index.exception; -import lombok.Builder; -import lombok.Data; +import org.springframework.http.HttpStatus; -import java.time.Duration; +public class PingDeniedException extends IndexException { -@Builder -@Data -public class EventsConfig { - private final Duration retrievalRateLimitWait; - private final Duration retrievalTimeout; - private final Duration pingValidDuration; - private final Duration pingRateLimitDuration; - private final int pingRateLimitHits; + public PingDeniedException(String clientUrl) { + super("Client URL is denied: " + clientUrl, HttpStatus.FORBIDDEN); + } } diff --git a/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettings.java b/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettings.java new file mode 100644 index 000000000..c3227bce6 --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettings.java @@ -0,0 +1,68 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.entity.index.settings; + +import lombok.*; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import javax.validation.constraints.NotNull; +import java.util.Objects; + +@Document +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder(toBuilder = true) +public class IndexSettings { + @Id + protected ObjectId id; + + @NotNull + private IndexSettingsRetrieval retrieval; + + @NotNull + private IndexSettingsPing ping; + + public static IndexSettings getDefault() { + IndexSettings settings = new IndexSettings(); + settings.setPing(IndexSettingsPing.getDefault()); + settings.setRetrieval(IndexSettingsRetrieval.getDefault()); + return settings; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexSettings that = (IndexSettings) o; + return retrieval.equals(that.retrieval) && ping.equals(that.ping); + } + + @Override + public int hashCode() { + return Objects.hash(retrieval, ping); + } +} diff --git a/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettingsPing.java b/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettingsPing.java new file mode 100644 index 000000000..32370ee23 --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettingsPing.java @@ -0,0 +1,59 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.entity.index.settings; + +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder(toBuilder = true) +@EqualsAndHashCode +public class IndexSettingsPing { + @NotNull + private Duration validDuration; + + @NotNull + private Duration rateLimitDuration; + + @NotNull + private Integer rateLimitHits; + + @NotNull + private List denyList; + + public static IndexSettingsPing getDefault() { + IndexSettingsPing ping = new IndexSettingsPing(); + ping.setValidDuration(Duration.ofDays(7)); + ping.setRateLimitDuration(Duration.ofHours(6)); + ping.setRateLimitHits(10); + ping.setDenyList(Collections.singletonList("^(http|https)://localhost(:[0-9]+){0,1}.*$")); + return ping; + } +} diff --git a/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettingsRetrieval.java b/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettingsRetrieval.java new file mode 100644 index 000000000..3aff145ca --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/entity/index/settings/IndexSettingsRetrieval.java @@ -0,0 +1,49 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.entity.index.settings; + +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder(toBuilder = true) +@EqualsAndHashCode +public class IndexSettingsRetrieval { + @NotNull + private Duration rateLimitWait; + + @NotNull + private Duration timeout; + + public static IndexSettingsRetrieval getDefault() { + IndexSettingsRetrieval retrieval = new IndexSettingsRetrieval(); + retrieval.setRateLimitWait(Duration.ofMinutes(10)); + retrieval.setTimeout(Duration.ofMinutes(1)); + return retrieval; + } +} diff --git a/src/main/java/nl/dtls/fairdatapoint/service/UtilityService.java b/src/main/java/nl/dtls/fairdatapoint/service/UtilityService.java new file mode 100644 index 000000000..c9b59867a --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/service/UtilityService.java @@ -0,0 +1,41 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.service; + +import nl.dtls.fairdatapoint.util.HttpUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; + +@Service +public class UtilityService { + + @Value("${instance.behindProxy:true}") + private Boolean behindProxy; + + public String getRemoteAddr(HttpServletRequest request) { + return HttpUtil.getClientIpAddress(request, behindProxy); + } +} diff --git a/src/main/java/nl/dtls/fairdatapoint/service/dashboard/DashboardService.java b/src/main/java/nl/dtls/fairdatapoint/service/dashboard/DashboardService.java index b1ce54197..84d676260 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/dashboard/DashboardService.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/dashboard/DashboardService.java @@ -72,17 +72,18 @@ public class DashboardService { public List getDashboard(IRI repositoryUri) throws MetadataServiceException { ResourceDefinition rd = resourceDefinitionService.getByUrlPrefix(""); Model repository = metadataService.retrieve(repositoryUri); - return getDashboardItem(repository, rd).getChildren(); + return getDashboardItem(repositoryUri, repository, rd).getChildren(); } - private DashboardItemDTO getDashboardItem(Model model, ResourceDefinition rd) throws MetadataServiceException { - IRI metadataUri = getUri(model); + private DashboardItemDTO getDashboardItem(IRI metadataUri, Model model, ResourceDefinition rd) throws MetadataServiceException { List children = new ArrayList<>(); for (ResourceDefinitionChild rdChild : rd.getChildren()) { IRI relationUri = i(rdChild.getRelationUri()); for (org.eclipse.rdf4j.model.Value childUri : getObjectsBy(model, metadataUri, relationUri)) { + IRI childIri = i(childUri.stringValue()); DashboardItemDTO child = getDashboardItem( - metadataService.retrieve(i(childUri.stringValue())), + childIri, + metadataService.retrieve(childIri), resourceDefinitionCache.getByUuid(rdChild.getResourceDefinitionUuid()) ); children.add(child); diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/entry/IndexEntryMapper.java b/src/main/java/nl/dtls/fairdatapoint/service/index/entry/IndexEntryMapper.java index bb516da87..b8f1b7b16 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/index/entry/IndexEntryMapper.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/entry/IndexEntryMapper.java @@ -25,7 +25,6 @@ import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryDTO; import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryDetailDTO; import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryStateDTO; -import nl.dtls.fairdatapoint.entity.index.config.EventsConfig; import nl.dtls.fairdatapoint.entity.index.entry.IndexEntry; import nl.dtls.fairdatapoint.entity.index.entry.IndexEntryState; import nl.dtls.fairdatapoint.entity.index.event.Event; @@ -40,27 +39,24 @@ @Service public class IndexEntryMapper { - @Autowired - private EventsConfig eventsConfig; - @Autowired private EventMapper eventMapper; - public IndexEntryDTO toDTO(IndexEntry indexEntry) { + public IndexEntryDTO toDTO(IndexEntry indexEntry, Instant validThreshold) { return new IndexEntryDTO( indexEntry.getUuid(), indexEntry.getClientUrl(), - toStateDTO(indexEntry.getState(), indexEntry.getLastRetrievalTime()), + toStateDTO(indexEntry.getState(), indexEntry.getLastRetrievalTime(), validThreshold), indexEntry.getRegistrationTime().toString(), indexEntry.getModificationTime().toString() ); } - public IndexEntryDetailDTO toDetailDTO(IndexEntry indexEntry, Iterable events) { + public IndexEntryDetailDTO toDetailDTO(IndexEntry indexEntry, Iterable events, Instant validThreshold) { return new IndexEntryDetailDTO( indexEntry.getUuid(), indexEntry.getClientUrl(), - toStateDTO(indexEntry.getState(), indexEntry.getLastRetrievalTime()), + toStateDTO(indexEntry.getState(), indexEntry.getLastRetrievalTime(), validThreshold), indexEntry.getCurrentMetadata(), StreamSupport.stream(events.spliterator(), false) .map(eventMapper::toDTO) @@ -71,10 +67,10 @@ public IndexEntryDetailDTO toDetailDTO(IndexEntry indexEntry, Iterable ev ); } - public IndexEntryStateDTO toStateDTO(IndexEntryState state, Instant lastRetrievalTime) { + public IndexEntryStateDTO toStateDTO(IndexEntryState state, Instant lastRetrievalTime, Instant validThreshold) { return switch (state) { case Unknown -> IndexEntryStateDTO.UNKNOWN; - case Valid -> lastRetrievalTime.isAfter(Instant.now().minus(eventsConfig.getPingValidDuration())) + case Valid -> lastRetrievalTime.isAfter(validThreshold) ? IndexEntryStateDTO.ACTIVE : IndexEntryStateDTO.INACTIVE; case Invalid -> IndexEntryStateDTO.INVALID; diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/entry/IndexEntryService.java b/src/main/java/nl/dtls/fairdatapoint/service/index/entry/IndexEntryService.java index 99dcd5bae..151030295 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/index/entry/IndexEntryService.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/entry/IndexEntryService.java @@ -23,26 +23,30 @@ package nl.dtls.fairdatapoint.service.index.entry; import lombok.extern.log4j.Log4j2; +import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryDTO; +import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryDetailDTO; import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryInfoDTO; import nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryStateDTO; import nl.dtls.fairdatapoint.api.dto.index.ping.PingDTO; import nl.dtls.fairdatapoint.database.mongo.repository.IndexEntryRepository; -import nl.dtls.fairdatapoint.entity.index.config.EventsConfig; +import nl.dtls.fairdatapoint.entity.exception.ResourceNotFoundException; import nl.dtls.fairdatapoint.entity.index.entry.IndexEntry; import nl.dtls.fairdatapoint.entity.index.entry.IndexEntryState; import nl.dtls.fairdatapoint.service.index.common.RequiredEnabledIndexFeature; +import nl.dtls.fairdatapoint.service.index.event.EventService; +import nl.dtls.fairdatapoint.service.index.settings.IndexSettingsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.validation.Valid; import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import static nl.dtls.fairdatapoint.api.dto.index.entry.IndexEntryStateDTO.*; @@ -55,21 +59,35 @@ public class IndexEntryService { private IndexEntryRepository repository; @Autowired - private EventsConfig eventsConfig; + private IndexSettingsService indexSettingsService; + @Autowired + private EventService eventService; + + @Autowired + private IndexEntryMapper mapper; + + @RequiredEnabledIndexFeature public Iterable getAllEntries() { return repository.findAll(); } + @RequiredEnabledIndexFeature + public List getAllEntriesAsDTOs() { + Instant validThreshold = getValidThreshold(); + return StreamSupport.stream(getAllEntries().spliterator(), true).map(it -> mapper.toDTO(it, validThreshold)).collect(Collectors.toList()); + } + @RequiredEnabledIndexFeature public Page getEntriesPage(Pageable pageable, String state) { + Instant validThreshold = getValidThreshold(); if (state.equalsIgnoreCase(ACTIVE.name())) { return repository.findAllByStateEqualsAndLastRetrievalTimeAfter(pageable, IndexEntryState.Valid, - getValidThreshold()); + validThreshold); } if (state.equalsIgnoreCase(IndexEntryStateDTO.INACTIVE.name())) { return repository.findAllByStateEqualsAndLastRetrievalTimeBefore(pageable, IndexEntryState.Valid, - getValidThreshold()); + validThreshold); } if (state.equalsIgnoreCase(IndexEntryStateDTO.UNREACHABLE.name())) { return repository.findAllByStateEquals(pageable, IndexEntryState.Unreachable); @@ -83,20 +101,33 @@ public Page getEntriesPage(Pageable pageable, String state) { return repository.findAll(pageable); } + @RequiredEnabledIndexFeature + public Page getEntriesPageDTOs(Pageable pageable, String state) { + Instant validThreshold = getValidThreshold(); + return getEntriesPage(pageable, state).map(it -> mapper.toDTO(it, validThreshold)); + } + @RequiredEnabledIndexFeature public Optional getEntry(String uuid) { return repository.findByUuid(uuid); } + @RequiredEnabledIndexFeature + public Optional getEntryDetailDTO(String uuid) { + Instant validThreshold = getValidThreshold(); + return getEntry(uuid).map(entry -> mapper.toDetailDTO(entry, eventService.getEvents(entry.getUuid()), validThreshold)); + } + @RequiredEnabledIndexFeature public IndexEntryInfoDTO getEntriesInfo() { + Instant validThreshold = getValidThreshold(); Map entriesCount = new HashMap<>(); entriesCount.put("ALL", repository.count()); entriesCount.put(UNKNOWN.name(), repository.countAllByStateEquals(IndexEntryState.Unknown)); entriesCount.put(ACTIVE.name(), - repository.countAllByStateEqualsAndLastRetrievalTimeAfter(IndexEntryState.Valid, getValidThreshold())); + repository.countAllByStateEqualsAndLastRetrievalTimeAfter(IndexEntryState.Valid, validThreshold)); entriesCount.put(INACTIVE.name(), - repository.countAllByStateEqualsAndLastRetrievalTimeBefore(IndexEntryState.Valid, getValidThreshold())); + repository.countAllByStateEqualsAndLastRetrievalTimeBefore(IndexEntryState.Valid, validThreshold)); entriesCount.put(UNREACHABLE.name(), repository.countAllByStateEquals(IndexEntryState.Unreachable)); entriesCount.put(INVALID.name(), repository.countAllByStateEquals(IndexEntryState.Invalid)); return new IndexEntryInfoDTO(entriesCount); @@ -124,8 +155,14 @@ public IndexEntry storeEntry(@Valid PingDTO pingDTO) { return repository.save(entry); } - private Instant getValidThreshold() { - return Instant.now().minus(eventsConfig.getPingValidDuration()); + @RequiredEnabledIndexFeature + @PreAuthorize("hasRole('ADMIN')") + public void deleteEntry(String uuid) { + IndexEntry entry = repository.findByUuid(uuid).orElseThrow(() -> new ResourceNotFoundException("Index entry not found")); + repository.delete(entry); } + private Instant getValidThreshold() { + return Instant.now().minus(indexSettingsService.getOrDefaults().getPing().getValidDuration()); + } } diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/event/EventMapper.java b/src/main/java/nl/dtls/fairdatapoint/service/index/event/EventMapper.java index 5866fc66d..c9b03b7be 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/index/event/EventMapper.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/event/EventMapper.java @@ -45,9 +45,9 @@ public EventDTO toDTO(Event event) { ); } - public Event toAdminTriggerEvent(HttpServletRequest request, Authentication authentication, String clientUrl) { + public Event toAdminTriggerEvent(HttpServletRequest request, Authentication authentication, String clientUrl, String remoteAddr) { var adminTrigger = new AdminTrigger(); - adminTrigger.setRemoteAddr(request.getRemoteAddr()); + adminTrigger.setRemoteAddr(remoteAddr); adminTrigger.setTokenName(authentication.getName()); adminTrigger.setClientUrl(clientUrl); return new Event(VERSION, adminTrigger); diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/event/EventService.java b/src/main/java/nl/dtls/fairdatapoint/service/index/event/EventService.java index ebd78038e..327d767f9 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/index/event/EventService.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/event/EventService.java @@ -28,24 +28,30 @@ import nl.dtls.fairdatapoint.database.mongo.repository.EventRepository; import nl.dtls.fairdatapoint.database.mongo.repository.IndexEntryRepository; import nl.dtls.fairdatapoint.entity.exception.ResourceNotFoundException; -import nl.dtls.fairdatapoint.entity.index.config.EventsConfig; import nl.dtls.fairdatapoint.entity.index.entry.IndexEntry; import nl.dtls.fairdatapoint.entity.index.entry.IndexEntryState; import nl.dtls.fairdatapoint.entity.index.event.Event; import nl.dtls.fairdatapoint.entity.index.event.EventType; import nl.dtls.fairdatapoint.entity.index.exception.IncorrectPingFormatException; +import nl.dtls.fairdatapoint.entity.index.exception.PingDeniedException; import nl.dtls.fairdatapoint.entity.index.exception.RateLimitException; import nl.dtls.fairdatapoint.entity.index.http.Exchange; import nl.dtls.fairdatapoint.entity.index.http.ExchangeState; +import nl.dtls.fairdatapoint.service.UtilityService; import nl.dtls.fairdatapoint.service.index.common.RequiredEnabledIndexFeature; import nl.dtls.fairdatapoint.service.index.entry.IndexEntryService; +import nl.dtls.fairdatapoint.service.index.settings.IndexSettingsService; import nl.dtls.fairdatapoint.service.index.webhook.WebhookService; +import nl.dtls.fairdatapoint.util.HttpUtil; import org.eclipse.rdf4j.util.iterators.EmptyIterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.security.core.Authentication; @@ -74,20 +80,24 @@ public class EventService { private IndexEntryRepository indexEntryRepository; @Autowired + @Lazy private IndexEntryService indexEntryService; @Autowired private WebhookService webhookService; @Autowired - private EventsConfig eventsConfig; + private EventMapper eventMapper; @Autowired - private EventMapper eventMapper; + private UtilityService utilityService; @Autowired private IncomingPingUtils incomingPingUtils; + @Autowired + private IndexSettingsService indexSettingsService; + public Iterable getEvents(IndexEntry indexEntry) { // TODO: make events pagination in the future return eventRepository.getAllByRelatedTo(indexEntry, PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, @@ -102,19 +112,26 @@ public Iterable getEvents(String indexEntryUuid) { @RequiredEnabledIndexFeature @SneakyThrows public Event acceptIncomingPing(PingDTO reqDto, HttpServletRequest request) { - var remoteAddr = request.getRemoteAddr(); - var rateLimitSince = Instant.now().minus(eventsConfig.getPingRateLimitDuration()); + var remoteAddr = utilityService.getRemoteAddr(request); + var pingSettings = indexSettingsService.getOrDefaults().getPing(); + + if (indexSettingsService.isPingDenied(reqDto)) { + logger.info("Received ping is denied"); + throw new PingDeniedException(reqDto.getClientUrl()); + } + + var rateLimitSince = Instant.now().minus(pingSettings.getRateLimitDuration()); var previousPings = eventRepository.findAllByIncomingPingExchangeRemoteAddrAndCreatedAfter(remoteAddr, rateLimitSince); - if (previousPings.size() > eventsConfig.getPingRateLimitHits()) { + if (previousPings.size() > pingSettings.getRateLimitHits()) { logger.warn("Rate limit for PING reached by {}", remoteAddr); throw new RateLimitException(String.format( "Rate limit reached for %s (max. %d per %s) - PING ignored", - remoteAddr, eventsConfig.getPingRateLimitHits(), eventsConfig.getPingRateLimitDuration().toString()) + remoteAddr, pingSettings.getRateLimitHits(), pingSettings.getRateLimitDuration().toString()) ); } - var event = incomingPingUtils.prepareEvent(reqDto, request); + var event = incomingPingUtils.prepareEvent(reqDto, request, remoteAddr); eventRepository.save(event); event.execute(); try { @@ -137,14 +154,15 @@ public Event acceptIncomingPing(PingDTO reqDto, HttpServletRequest request) { } private void processMetadataRetrieval(Event event) { - String clientUrl = event.getRelatedTo().getClientUrl(); - if (MetadataRetrievalUtils.shouldRetrieve(event, eventsConfig.getRetrievalRateLimitWait())) { + var retrievalSettings = indexSettingsService.getOrDefaults().getRetrieval(); + var clientUrl = event.getRelatedTo().getClientUrl(); + if (MetadataRetrievalUtils.shouldRetrieve(event, retrievalSettings.getRateLimitWait())) { indexEntryRepository.save(event.getRelatedTo()); eventRepository.save(event); event.execute(); logger.info("Retrieving metadata for {}", clientUrl); - MetadataRetrievalUtils.retrieveRepositoryMetadata(event, eventsConfig.getRetrievalTimeout()); + MetadataRetrievalUtils.retrieveRepositoryMetadata(event, retrievalSettings.getTimeout()); Exchange ex = event.getMetadataRetrieval().getExchange(); if (ex.getState() == ExchangeState.Retrieved) { try { @@ -224,16 +242,19 @@ public void startResumeUnfinishedEvents() { } @RequiredEnabledIndexFeature - public Event acceptAdminTrigger(HttpServletRequest request, String indexEntryUuid) { + public Event acceptAdminTrigger(HttpServletRequest request, PingDTO pingDTO) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Event event = eventMapper.toAdminTriggerEvent(request, authentication, indexEntryUuid); - if (indexEntryUuid != null) { - Optional entry = indexEntryService.getEntry(indexEntryUuid); - if (entry.isEmpty()) { - throw new ResourceNotFoundException("There is no such entry: " + indexEntryUuid); - } - event.setRelatedTo(entry.get()); - } + Event event = eventMapper.toAdminTriggerEvent(request, authentication, pingDTO.getClientUrl(), utilityService.getRemoteAddr(request)); + IndexEntry entry = indexEntryService.storeEntry(pingDTO); + event.setRelatedTo(entry); + event.finish(); + return eventRepository.save(event); + } + + @RequiredEnabledIndexFeature + public Event acceptAdminTriggerAll(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Event event = eventMapper.toAdminTriggerEvent(request, authentication, null, utilityService.getRemoteAddr(request)); event.finish(); return eventRepository.save(event); } diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/event/IncomingPingUtils.java b/src/main/java/nl/dtls/fairdatapoint/service/index/event/IncomingPingUtils.java index 4f9ae523a..762c39f41 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/index/event/IncomingPingUtils.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/event/IncomingPingUtils.java @@ -43,9 +43,9 @@ public class IncomingPingUtils { @Autowired private ObjectMapper objectMapper; - public Event prepareEvent(PingDTO reqDto, HttpServletRequest request) { + public Event prepareEvent(PingDTO reqDto, HttpServletRequest request, String remoteAddr) { var incomingPing = new IncomingPing(); - var ex = new Exchange(ExchangeDirection.INCOMING, request.getRemoteAddr()); + var ex = new Exchange(ExchangeDirection.INCOMING, remoteAddr); incomingPing.setExchange(ex); ex.getRequest().setHeaders(getHeaders(request)); diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/settings/IndexSettingsMapper.java b/src/main/java/nl/dtls/fairdatapoint/service/index/settings/IndexSettingsMapper.java new file mode 100644 index 000000000..b2a3ed226 --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/settings/IndexSettingsMapper.java @@ -0,0 +1,98 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.service.index.settings; + +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsPingDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsRetrievalDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsUpdateDTO; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettings; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettingsPing; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettingsRetrieval; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +@Service +public class IndexSettingsMapper { + + private IndexSettingsPingDTO toPingDTO(IndexSettingsPing indexSettingsPing) { + return new IndexSettingsPingDTO( + indexSettingsPing.getValidDuration().toString(), + indexSettingsPing.getRateLimitDuration().toString(), + indexSettingsPing.getRateLimitHits(), + indexSettingsPing.getDenyList() + ); + } + + private IndexSettingsRetrievalDTO toRetrievalDTO(IndexSettingsRetrieval indexSettingsRetrieval) { + return new IndexSettingsRetrievalDTO( + indexSettingsRetrieval.getRateLimitWait().toString(), + indexSettingsRetrieval.getTimeout().toString() + ); + } + + public IndexSettingsDTO toDTO(IndexSettings indexSettings) { + return new IndexSettingsDTO( + toRetrievalDTO(indexSettings.getRetrieval()), + toPingDTO(indexSettings.getPing()), + indexSettings.equals(IndexSettings.getDefault()) + ); + } + + public IndexSettingsUpdateDTO toUpdateDTO(IndexSettings indexSettings) { + return new IndexSettingsUpdateDTO( + toRetrievalDTO(indexSettings.getRetrieval()), + toPingDTO(indexSettings.getPing()) + ); + } + + private IndexSettingsPing fromDTO(IndexSettingsPingDTO dto, IndexSettingsPing ping) { + return + ping + .toBuilder() + .validDuration(Duration.parse(dto.getValidDuration())) + .rateLimitDuration(Duration.parse(dto.getRateLimitDuration())) + .rateLimitHits(dto.getRateLimitHits()) + .denyList(dto.getDenyList()) + .build(); + } + + private IndexSettingsRetrieval fromDTO(IndexSettingsRetrievalDTO dto, IndexSettingsRetrieval retrieval) { + return + retrieval + .toBuilder() + .rateLimitWait(Duration.parse(dto.getRateLimitWait())) + .timeout(Duration.parse(dto.getTimeout())) + .build(); + } + + public IndexSettings fromUpdateDTO(IndexSettingsUpdateDTO dto, IndexSettings indexSettings) { + return + indexSettings + .toBuilder() + .ping(fromDTO(dto.getPing(), indexSettings.getPing())) + .retrieval(fromDTO(dto.getRetrieval(), indexSettings.getRetrieval())) + .build(); + } +} diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/settings/IndexSettingsService.java b/src/main/java/nl/dtls/fairdatapoint/service/index/settings/IndexSettingsService.java new file mode 100644 index 000000000..03c2c9f3f --- /dev/null +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/settings/IndexSettingsService.java @@ -0,0 +1,73 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.service.index.settings; + +import nl.dtls.fairdatapoint.api.dto.index.ping.PingDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsUpdateDTO; +import nl.dtls.fairdatapoint.database.mongo.repository.IndexSettingsRepository; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettings; +import nl.dtls.fairdatapoint.service.index.common.RequiredEnabledIndexFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.regex.Pattern; + +@Service +public class IndexSettingsService { + private static final Logger logger = LoggerFactory.getLogger(IndexSettingsService.class); + + @Autowired + private IndexSettingsRepository repository; + + @Autowired + private IndexSettingsMapper mapper; + + @RequiredEnabledIndexFeature + public boolean isPingDenied(PingDTO ping) { + logger.info("Checking if ping.clientUrl is on deny list: " + ping.getClientUrl()); + return getOrDefaults().getPing().getDenyList().parallelStream().anyMatch(pattern -> Pattern.matches(pattern, ping.getClientUrl())); + } + + @RequiredEnabledIndexFeature + public IndexSettings getOrDefaults() { + return repository.findFirstBy().orElse(IndexSettings.getDefault()); + } + + @RequiredEnabledIndexFeature + public IndexSettingsDTO getCurrentSettings() { + return mapper.toDTO(getOrDefaults()); + } + + @RequiredEnabledIndexFeature + public IndexSettingsDTO updateSettings(IndexSettingsUpdateDTO dto) { + return mapper.toDTO(repository.save(mapper.fromUpdateDTO(dto, getOrDefaults()))); + } + + @RequiredEnabledIndexFeature + public IndexSettingsDTO resetSettings() { + return updateSettings(mapper.toUpdateDTO(IndexSettings.getDefault())); + } +} diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/webhook/WebhookMapper.java b/src/main/java/nl/dtls/fairdatapoint/service/index/webhook/WebhookMapper.java index b0e26546c..5b888d891 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/index/webhook/WebhookMapper.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/webhook/WebhookMapper.java @@ -47,10 +47,10 @@ public Event toTriggerEvent(Webhook webhook, WebhookEvent webhookEvent, Event tr return new Event(VERSION, webhookTrigger, triggerEvent); } - public Event toPingEvent(HttpServletRequest request, Authentication authentication, UUID webhookUuid) { + public Event toPingEvent(HttpServletRequest request, Authentication authentication, UUID webhookUuid, String remoteAddr) { var webhookPing = new WebhookPing(); webhookPing.setWebhookUuid(webhookUuid); - webhookPing.setRemoteAddr(request.getRemoteAddr()); + webhookPing.setRemoteAddr(remoteAddr); webhookPing.setTokenName(authentication.getName()); return new Event(VERSION, webhookPing); } diff --git a/src/main/java/nl/dtls/fairdatapoint/service/index/webhook/WebhookService.java b/src/main/java/nl/dtls/fairdatapoint/service/index/webhook/WebhookService.java index 73ed5a8fb..ab1ea736a 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/index/webhook/WebhookService.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/index/webhook/WebhookService.java @@ -28,11 +28,12 @@ import nl.dtls.fairdatapoint.database.mongo.repository.EventRepository; import nl.dtls.fairdatapoint.database.mongo.repository.WebhookRepository; import nl.dtls.fairdatapoint.entity.exception.ResourceNotFoundException; -import nl.dtls.fairdatapoint.entity.index.config.EventsConfig; import nl.dtls.fairdatapoint.entity.index.event.Event; import nl.dtls.fairdatapoint.entity.index.webhook.Webhook; import nl.dtls.fairdatapoint.entity.index.webhook.WebhookEvent; +import nl.dtls.fairdatapoint.service.UtilityService; import nl.dtls.fairdatapoint.service.index.common.RequiredEnabledIndexFeature; +import nl.dtls.fairdatapoint.service.index.settings.IndexSettingsService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -64,12 +65,16 @@ public class WebhookService { EventRepository eventRepository; @Autowired - private EventsConfig eventsConfig; + private IndexSettingsService indexSettingsService; + + @Autowired + private UtilityService utilityService; private static final String SECRET_PLACEHOLDER = "*** HIDDEN ***"; @RequiredEnabledIndexFeature public void processWebhookTrigger(Event event) { + var retrievalSettings = indexSettingsService.getOrDefaults().getRetrieval(); event.execute(); eventRepository.save(event); WebhookPayloadDTO webhookPayload = webhookMapper.toWebhookPayloadDTO(event); @@ -78,7 +83,7 @@ public void processWebhookTrigger(Event event) { String signature = WebhookUtils.computeHashSignature(payloadWithSecret); webhookPayload.setSecret(SECRET_PLACEHOLDER); String payloadWithoutSecret = objectMapper.writeValueAsString(webhookPayload); - WebhookUtils.postWebhook(event, eventsConfig.getRetrievalTimeout(), payloadWithoutSecret, signature); + WebhookUtils.postWebhook(event, retrievalSettings.getTimeout(), payloadWithoutSecret, signature); } catch (JsonProcessingException e) { logger.error("Failed to convert webhook payload to string"); } catch (NoSuchAlgorithmException e) { @@ -106,7 +111,7 @@ public void triggerWebhooks(WebhookEvent webhookEvent, Event triggerEvent) { public Event handleWebhookPing(HttpServletRequest request, UUID webhookUuid) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Optional webhook = webhookRepository.findByUuid(webhookUuid); - Event event = eventRepository.save(webhookMapper.toPingEvent(request, authentication, webhookUuid)); + Event event = eventRepository.save(webhookMapper.toPingEvent(request, authentication, webhookUuid, utilityService.getRemoteAddr(request))); if (webhook.isEmpty()) { throw new ResourceNotFoundException("There is no such webhook: " + webhookUuid); } @@ -128,17 +133,10 @@ public void triggerWebhooks(Event triggerEvent) { break; case MetadataRetrieval: switch (triggerEvent.getRelatedTo().getState()) { - case Valid: - triggerWebhooks(WebhookEvent.EntryValid, triggerEvent); - break; - case Invalid: - triggerWebhooks(WebhookEvent.EntryInvalid, triggerEvent); - break; - case Unreachable: - triggerWebhooks(WebhookEvent.EntryUnreachable, triggerEvent); - break; - default: - logger.warn("Invalid state of MetadataRetrieval: {}", triggerEvent.getRelatedTo().getState()); + case Valid -> triggerWebhooks(WebhookEvent.EntryValid, triggerEvent); + case Invalid -> triggerWebhooks(WebhookEvent.EntryInvalid, triggerEvent); + case Unreachable -> triggerWebhooks(WebhookEvent.EntryUnreachable, triggerEvent); + default -> logger.warn("Invalid state of MetadataRetrieval: {}", triggerEvent.getRelatedTo().getState()); } break; case WebhookPing: diff --git a/src/main/java/nl/dtls/fairdatapoint/service/metadata/state/MetadataStateService.java b/src/main/java/nl/dtls/fairdatapoint/service/metadata/state/MetadataStateService.java index e18ad2dae..29955f5b1 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/metadata/state/MetadataStateService.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/metadata/state/MetadataStateService.java @@ -68,14 +68,13 @@ public Metadata get(IRI metadataUri) { return oMetadata.get(); } - public MetaStateDTO getState(Model model, ResourceDefinition rd) { + public MetaStateDTO getState(IRI metadataUri, Model model, ResourceDefinition rd) { // 1. Return null if user is not log in if (currentUserService.getCurrentUser().isEmpty()) { return null; } // 2. Get metadata info for current - IRI metadataUri = getUri(model); Optional oMetadata = metadataRepository.findByUri(metadataUri.stringValue()); if (oMetadata.isEmpty()) { throw new ResourceNotFoundException(format("Metadata info '%s' was not found", metadataUri)); diff --git a/src/main/java/nl/dtls/fairdatapoint/service/profile/ProfileService.java b/src/main/java/nl/dtls/fairdatapoint/service/profile/ProfileService.java index f6b18349b..318f42805 100644 --- a/src/main/java/nl/dtls/fairdatapoint/service/profile/ProfileService.java +++ b/src/main/java/nl/dtls/fairdatapoint/service/profile/ProfileService.java @@ -1,3 +1,25 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package nl.dtls.fairdatapoint.service.profile; import nl.dtls.fairdatapoint.service.shape.ShapeService; diff --git a/src/main/java/nl/dtls/fairdatapoint/util/HttpUtil.java b/src/main/java/nl/dtls/fairdatapoint/util/HttpUtil.java index d18cab685..03c81ed80 100644 --- a/src/main/java/nl/dtls/fairdatapoint/util/HttpUtil.java +++ b/src/main/java/nl/dtls/fairdatapoint/util/HttpUtil.java @@ -40,16 +40,43 @@ @Slf4j public class HttpUtil { + private static final String[] IP_HEADER_CANDIDATES = { + "X-Forwarded-For", + "X-Real-IP", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "HTTP_VIA", + "REMOTE_ADDR" + }; + + public static String getClientIpAddress(HttpServletRequest request, Boolean behindProxy) { + if (behindProxy) { + for (String header : IP_HEADER_CANDIDATES) { + String ipList = request.getHeader(header); + if (ipList != null && ipList.length() != 0 && !"unknown".equalsIgnoreCase(ipList)) { + return ipList.split(",")[0]; + } + } + } + return request.getRemoteAddr(); + } + public static String getRequestURL(HttpServletRequest request, String persistentUrl) { String urlS = request.getRequestURL().toString(); - log.info("Original requesed url {}", urlS); + log.info("Original requested url {}", urlS); try { urlS = removeLastSlash(urlS.replace("/expanded", "")); persistentUrl = removeLastSlash(persistentUrl); URL url = new URL(urlS); String modifiedUrl = persistentUrl + url.getPath(); - log.info("Modified requesed url {}", modifiedUrl); + log.info("Modified requested url {}", modifiedUrl); return modifiedUrl; diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index bcdcf50fd..6735ab40b 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -59,6 +59,11 @@ "name": "fdp-index.api.contactName", "type": "java.lang.String", "description": "Contact name/label for OpenAPI docs" + }, + { + "name": "instance.behindProxy", + "type": "java.lang.String", + "description": "If FDP is running behind reverse HTTP proxy" } ] -} \ No newline at end of file +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1c5cdd3be..70ffbaaf6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,6 @@ instance: clientUrl: http://localhost:8080 + behindProxy: true spring: data: @@ -59,11 +60,3 @@ metadataMetrics: fdp-index: enabled: false - events: - retrieval: - rateLimitWait: PT10M # 10 minutes (ISO 8601) - timeout: PT1M # 1 minute (ISO 8601) - ping: - validDuration: P7D # 7 days (ISO 8601) - rateLimitDuration: PT6H - rateLimitHits: 10 diff --git a/src/test/java/nl/dtls/fairdatapoint/acceptance/index/admin/List_TriggerAll_POST.java b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/admin/List_TriggerAll_POST.java new file mode 100644 index 000000000..307005646 --- /dev/null +++ b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/admin/List_TriggerAll_POST.java @@ -0,0 +1,130 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.acceptance.index.admin; + +import nl.dtls.fairdatapoint.WebIntegrationTest; +import nl.dtls.fairdatapoint.api.dto.index.ping.PingDTO; +import nl.dtls.fairdatapoint.database.mongo.repository.EventRepository; +import nl.dtls.fairdatapoint.database.mongo.repository.IndexEntryRepository; +import nl.dtls.fairdatapoint.entity.index.entry.IndexEntry; +import nl.dtls.fairdatapoint.entity.index.event.Event; +import nl.dtls.fairdatapoint.entity.index.event.EventType; +import nl.dtls.fairdatapoint.utils.TestIndexEntryFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; + +import java.net.URI; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; + +@DisplayName("POST /index/admin/trigger-all") +public class List_TriggerAll_POST extends WebIntegrationTest { + + @Autowired + private EventRepository eventRepository; + + @Autowired + private IndexEntryRepository indexEntryRepository; + + private final ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() { + }; + + private URI url() { + return URI.create("/index/admin/trigger-all"); + } + + @Test + @DisplayName("HTTP 403: no token") + public void res403_noToken() { + // GIVEN + RequestEntity request = RequestEntity + .post(url()) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN: + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + } + + @Test + @DisplayName("HTTP 403: incorrect token") + public void res403_incorrectToken() { + // GIVEN + RequestEntity request = RequestEntity + .post(url()) + .header(HttpHeaders.AUTHORIZATION, "mySecretToken") + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN: + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + } + + @Test + @DisplayName("HTTP 403: non-admin token") + public void res403_nonAdminToken() { + // GIVEN + RequestEntity request = RequestEntity + .post(url()) + .header(HttpHeaders.AUTHORIZATION, ALBERT_TOKEN) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN: + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + } + + @Test + @DisplayName("HTTP 204: trigger all") + public void res204_triggerAll() { + // GIVEN: prepare request + RequestEntity request = RequestEntity + .post(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + List events = eventRepository.getAllByType(EventType.AdminTrigger); + + // THEN: + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.NO_CONTENT))); + assertThat("One AdminTrigger event is created", events.size(), is(equalTo(1))); + assertThat("Records correct client URL as null", events.get(0).getAdminTrigger().getClientUrl(), is(equalTo(null))); + } +} diff --git a/src/test/java/nl/dtls/fairdatapoint/acceptance/index/admin/List_Trigger_POST.java b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/admin/List_Trigger_POST.java index 11207073a..651036181 100644 --- a/src/test/java/nl/dtls/fairdatapoint/acceptance/index/admin/List_Trigger_POST.java +++ b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/admin/List_Trigger_POST.java @@ -23,6 +23,7 @@ package nl.dtls.fairdatapoint.acceptance.index.admin; import nl.dtls.fairdatapoint.WebIntegrationTest; +import nl.dtls.fairdatapoint.api.dto.index.ping.PingDTO; import nl.dtls.fairdatapoint.database.mongo.repository.EventRepository; import nl.dtls.fairdatapoint.database.mongo.repository.IndexEntryRepository; import nl.dtls.fairdatapoint.entity.index.entry.IndexEntry; @@ -32,6 +33,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -61,8 +63,10 @@ private URI url() { return URI.create("/index/admin/trigger"); } - private URI url(String clientUrl) { - return URI.create("/index/admin/trigger?clientUrl=" + clientUrl); + private PingDTO reqDTO(String clientUrl) { + PingDTO dto = new PingDTO(); + dto.setClientUrl(clientUrl); + return dto; } @Test @@ -70,11 +74,12 @@ private URI url(String clientUrl) { public void res403_noToken() { // GIVEN: prepare data String clientUrl = "http://example.com"; + PingDTO reqDTO = reqDTO(clientUrl); // AND: prepare request - RequestEntity request = RequestEntity - .post(url(clientUrl)) - .build(); + RequestEntity request = RequestEntity + .post(url()) + .body(reqDTO); // WHEN ResponseEntity result = client.exchange(request, responseType); @@ -88,12 +93,13 @@ public void res403_noToken() { public void res403_incorrectToken() { // GIVEN: prepare data String clientUrl = "http://example.com"; + PingDTO reqDTO = reqDTO(clientUrl); // AND: prepare request - RequestEntity request = RequestEntity - .post(url(clientUrl)) - .header(HttpHeaders.AUTHORIZATION, ALBERT_TOKEN) - .build(); + RequestEntity request = RequestEntity + .post(url()) + .header(HttpHeaders.AUTHORIZATION, "mySecretToken") + .body(reqDTO); // WHEN ResponseEntity result = client.exchange(request, responseType); @@ -107,12 +113,13 @@ public void res403_incorrectToken() { public void res403_nonAdminToken() { // GIVEN: prepare data String clientUrl = "http://example.com"; + PingDTO reqDTO = reqDTO(clientUrl); // AND: prepare request - RequestEntity request = RequestEntity - .post(url(clientUrl)) + RequestEntity request = RequestEntity + .post(url()) .header(HttpHeaders.AUTHORIZATION, ALBERT_TOKEN) - .build(); + .body(reqDTO); // WHEN ResponseEntity result = client.exchange(request, responseType); @@ -122,37 +129,39 @@ public void res403_nonAdminToken() { } @Test - @DisplayName("HTTP 204: trigger one") - public void res204_triggerOne() { + @DisplayName("HTTP 400: malformed URL") + public void res403_malformedUrl() { // GIVEN: prepare data - IndexEntry entry = TestIndexEntryFixtures.entryExample(); - indexEntryRepository.save(entry); + String clientUrl = "http://example.com"; + PingDTO reqDTO = reqDTO(clientUrl); + reqDTO.setClientUrl("thisIsNot/Url"); // AND: prepare request - RequestEntity request = RequestEntity - .post(url(entry.getUuid())) + RequestEntity request = RequestEntity + .post(url()) .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) - .build(); + .body(reqDTO); // WHEN ResponseEntity result = client.exchange(request, responseType); - List events = eventRepository.getAllByType(EventType.AdminTrigger); - // THEN: - assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.NO_CONTENT))); - assertThat("One AdminTrigger event is created", events.size(), is(equalTo(1))); - assertThat("Records correct client URL", events.get(0).getAdminTrigger().getClientUrl(), - is(equalTo(entry.getUuid()))); + // THEN: + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.BAD_REQUEST))); } @Test - @DisplayName("HTTP 204: trigger all") - public void res204_triggerAll() { - // GIVEN: prepare request - RequestEntity request = RequestEntity + @DisplayName("HTTP 204: trigger one") + public void res204_triggerOne() { + // GIVEN: prepare data + IndexEntry entry = TestIndexEntryFixtures.entryExample(); + indexEntryRepository.save(entry); + PingDTO reqDTO = reqDTO(entry.getClientUrl()); + + // AND: prepare request + RequestEntity request = RequestEntity .post(url()) .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) - .build(); + .body(reqDTO); // WHEN ResponseEntity result = client.exchange(request, responseType); @@ -161,8 +170,7 @@ public void res204_triggerAll() { // THEN: assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.NO_CONTENT))); assertThat("One AdminTrigger event is created", events.size(), is(equalTo(1))); - assertThat("Records correct client URL as null", events.get(0).getAdminTrigger().getClientUrl(), - is(equalTo(null))); + assertThat("Records correct client URL", events.get(0).getAdminTrigger().getClientUrl(), is(equalTo(entry.getClientUrl()))); } @Test @@ -170,17 +178,21 @@ public void res204_triggerAll() { public void res404_triggerOne() { // GIVEN: prepare data IndexEntry entry = TestIndexEntryFixtures.entryExample(); + PingDTO reqDTO = reqDTO(entry.getClientUrl()); // AND: prepare request - RequestEntity request = RequestEntity - .post(url(entry.getUuid())) + RequestEntity request = RequestEntity + .post(url()) .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) - .build(); + .body(reqDTO); // WHEN ResponseEntity result = client.exchange(request, responseType); + List events = eventRepository.getAllByType(EventType.AdminTrigger); // THEN: - assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.NOT_FOUND))); + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.NO_CONTENT))); + assertThat("One AdminTrigger event is created", events.size(), is(equalTo(1))); + assertThat("Records correct client URL", events.get(0).getAdminTrigger().getClientUrl(), is(equalTo(entry.getClientUrl()))); } } diff --git a/src/test/java/nl/dtls/fairdatapoint/acceptance/index/ping/List_POST.java b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/ping/List_POST.java index e17336677..c21951442 100644 --- a/src/test/java/nl/dtls/fairdatapoint/acceptance/index/ping/List_POST.java +++ b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/ping/List_POST.java @@ -43,7 +43,7 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsEqual.equalTo; -@DisplayName("POST /index") +@DisplayName("POST /") public class List_POST extends WebIntegrationTest { @Autowired diff --git a/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_DELETE.java b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_DELETE.java new file mode 100644 index 000000000..7fd318fa5 --- /dev/null +++ b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_DELETE.java @@ -0,0 +1,175 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.acceptance.index.settings; + +import nl.dtls.fairdatapoint.WebIntegrationTest; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsDTO; +import nl.dtls.fairdatapoint.database.mongo.repository.IndexSettingsRepository; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettings; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettingsPing; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettingsRetrieval; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; + +import java.net.URI; +import java.time.Duration; +import java.util.Collections; +import java.util.Objects; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsNull.notNullValue; + +@DisplayName("DELETE /index/settings") +public class List_DELETE extends WebIntegrationTest { + + @Autowired + private IndexSettingsRepository indexSettingsRepository; + + private final ParameterizedTypeReference responseType = + new ParameterizedTypeReference<>() { + }; + + private URI url() { + return URI.create("/index/settings"); + } + + private IndexSettings customSettings() { + return new IndexSettings() + .toBuilder() + .ping( + new IndexSettingsPing() + .toBuilder() + .denyList(Collections.singletonList("http://localhost.*$")) + .rateLimitDuration(Duration.ofMinutes(17)) + .validDuration(Duration.ofDays(5)) + .rateLimitHits(666) + .build() + ) + .retrieval( + new IndexSettingsRetrieval() + .toBuilder() + .rateLimitWait(Duration.ofHours(16)) + .timeout(Duration.ofSeconds(55)) + .build() + ) + .build(); + } + + @Test + @DisplayName("HTTP 200: default settings") + public void res200_defaultSettings() { + // GIVEN: prepare data + IndexSettings settings = IndexSettings.getDefault(); + indexSettingsRepository.deleteAll(); + + // AND: prepare request + RequestEntity request = RequestEntity + .delete(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .accept(MediaType.APPLICATION_JSON) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.OK))); + assertThat("Response body is not null", result.getBody(), is(notNullValue())); + assertThat("Response contains default valid duration", Objects.requireNonNull(result.getBody()).getPing().getValidDuration(), is(equalTo(settings.getPing().getValidDuration().toString()))); + assertThat("Response contains default rate limit duration", Objects.requireNonNull(result.getBody()).getPing().getRateLimitDuration(), is(equalTo(settings.getPing().getRateLimitDuration().toString()))); + assertThat("Response contains default rate limit hits", Objects.requireNonNull(result.getBody()).getPing().getRateLimitHits(), is(equalTo(settings.getPing().getRateLimitHits()))); + assertThat("Response contains default deny list", Objects.requireNonNull(result.getBody()).getPing().getDenyList(), is(equalTo(settings.getPing().getDenyList()))); + assertThat("Response contains default timeout", Objects.requireNonNull(result.getBody()).getRetrieval().getTimeout(), is(equalTo(settings.getRetrieval().getTimeout().toString()))); + assertThat("Response contains default rate limit wait", Objects.requireNonNull(result.getBody()).getRetrieval().getRateLimitWait(), is(equalTo(settings.getRetrieval().getRateLimitWait().toString()))); + assertThat("Response indicated default settings", Objects.requireNonNull(result.getBody()).getIsDefault(), is(Boolean.TRUE)); + } + + @Test + @DisplayName("HTTP 200: custom settings") + public void res200_customSettings() { + // GIVEN: prepare data + IndexSettings settings = IndexSettings.getDefault(); + IndexSettings customSettings = customSettings(); + indexSettingsRepository.deleteAll(); + indexSettingsRepository.insert(customSettings); + + // AND: prepare request + RequestEntity request = RequestEntity + .delete(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .accept(MediaType.APPLICATION_JSON) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.OK))); + assertThat("Response body is not null", result.getBody(), is(notNullValue())); + assertThat("Response contains default valid duration", Objects.requireNonNull(result.getBody()).getPing().getValidDuration(), is(equalTo(settings.getPing().getValidDuration().toString()))); + assertThat("Response contains default rate limit duration", Objects.requireNonNull(result.getBody()).getPing().getRateLimitDuration(), is(equalTo(settings.getPing().getRateLimitDuration().toString()))); + assertThat("Response contains default rate limit hits", Objects.requireNonNull(result.getBody()).getPing().getRateLimitHits(), is(equalTo(settings.getPing().getRateLimitHits()))); + assertThat("Response contains default deny list", Objects.requireNonNull(result.getBody()).getPing().getDenyList(), is(equalTo(settings.getPing().getDenyList()))); + assertThat("Response contains default timeout", Objects.requireNonNull(result.getBody()).getRetrieval().getTimeout(), is(equalTo(settings.getRetrieval().getTimeout().toString()))); + assertThat("Response contains default rate limit wait", Objects.requireNonNull(result.getBody()).getRetrieval().getRateLimitWait(), is(equalTo(settings.getRetrieval().getRateLimitWait().toString()))); + assertThat("Response indicated default settings", Objects.requireNonNull(result.getBody()).getIsDefault(), is(Boolean.TRUE)); + } + + @Test + @DisplayName("HTTP 403: no token") + public void res403_noToken() { + // GIVEN + RequestEntity request = RequestEntity + .delete(url()) + .accept(MediaType.APPLICATION_JSON) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("It is forbidden without auth", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + } + + @Test + @DisplayName("HTTP 403: not admin") + public void res403_notAdmin() { + // GIVEN + RequestEntity request = RequestEntity + .delete(url()) + .header(HttpHeaders.AUTHORIZATION, NIKOLA_TOKEN) + .accept(MediaType.APPLICATION_JSON) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("It is forbidden for non-admin users", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + } +} diff --git a/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_GET.java b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_GET.java new file mode 100644 index 000000000..574830827 --- /dev/null +++ b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_GET.java @@ -0,0 +1,176 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.acceptance.index.settings; + +import nl.dtls.fairdatapoint.WebIntegrationTest; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsDTO; +import nl.dtls.fairdatapoint.database.mongo.repository.IndexSettingsRepository; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettings; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettingsPing; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettingsRetrieval; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; + +import java.net.URI; +import java.time.Duration; +import java.util.Collections; +import java.util.Objects; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsNull.notNullValue; + +@DisplayName("GET /index/settings") +public class List_GET extends WebIntegrationTest { + + @Autowired + private IndexSettingsRepository indexSettingsRepository; + + private final ParameterizedTypeReference responseType = + new ParameterizedTypeReference<>() { + }; + + private URI url() { + return URI.create("/index/settings"); + } + + private IndexSettings customSettings() { + return new IndexSettings() + .toBuilder() + .ping( + new IndexSettingsPing() + .toBuilder() + .denyList(Collections.singletonList("http://localhost.*$")) + .rateLimitDuration(Duration.ofMinutes(17)) + .validDuration(Duration.ofDays(5)) + .rateLimitHits(666) + .build() + ) + .retrieval( + new IndexSettingsRetrieval() + .toBuilder() + .rateLimitWait(Duration.ofHours(16)) + .timeout(Duration.ofSeconds(55)) + .build() + ) + .build(); + } + + @Test + @DisplayName("HTTP 200: default settings") + public void res200_defaultSettings() { + // GIVEN: prepare data + IndexSettings settings = IndexSettings.getDefault(); + indexSettingsRepository.deleteAll(); + + // AND: prepare request + RequestEntity request = RequestEntity + .get(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .accept(MediaType.APPLICATION_JSON) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("No settings are created", indexSettingsRepository.findAll().size(), is(equalTo(0))); + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.OK))); + assertThat("Response body is not null", result.getBody(), is(notNullValue())); + assertThat("Response contains default valid duration", Objects.requireNonNull(result.getBody()).getPing().getValidDuration(), is(equalTo(settings.getPing().getValidDuration().toString()))); + assertThat("Response contains default rate limit duration", Objects.requireNonNull(result.getBody()).getPing().getRateLimitDuration(), is(equalTo(settings.getPing().getRateLimitDuration().toString()))); + assertThat("Response contains default rate limit hits", Objects.requireNonNull(result.getBody()).getPing().getRateLimitHits(), is(equalTo(settings.getPing().getRateLimitHits()))); + assertThat("Response contains default deny list", Objects.requireNonNull(result.getBody()).getPing().getDenyList(), is(equalTo(settings.getPing().getDenyList()))); + assertThat("Response contains default timeout", Objects.requireNonNull(result.getBody()).getRetrieval().getTimeout(), is(equalTo(settings.getRetrieval().getTimeout().toString()))); + assertThat("Response contains default rate limit wait", Objects.requireNonNull(result.getBody()).getRetrieval().getRateLimitWait(), is(equalTo(settings.getRetrieval().getRateLimitWait().toString()))); + assertThat("Response indicated default settings", Objects.requireNonNull(result.getBody()).getIsDefault(), is(Boolean.TRUE)); + } + + @Test + @DisplayName("HTTP 200: custom settings") + public void res200_customSettings() { + // GIVEN: prepare data + IndexSettings settings = customSettings(); + indexSettingsRepository.deleteAll(); + indexSettingsRepository.insert(settings); + + // AND: prepare request + RequestEntity request = RequestEntity + .get(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .accept(MediaType.APPLICATION_JSON) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("No settings are created", indexSettingsRepository.findAll().size(), is(equalTo(1))); + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.OK))); + assertThat("Response body is not null", result.getBody(), is(notNullValue())); + assertThat("Response contains custom valid duration", Objects.requireNonNull(result.getBody()).getPing().getValidDuration(), is(equalTo(settings.getPing().getValidDuration().toString()))); + assertThat("Response contains custom rate limit duration", Objects.requireNonNull(result.getBody()).getPing().getRateLimitDuration(), is(equalTo(settings.getPing().getRateLimitDuration().toString()))); + assertThat("Response contains custom rate limit hits", Objects.requireNonNull(result.getBody()).getPing().getRateLimitHits(), is(equalTo(settings.getPing().getRateLimitHits()))); + assertThat("Response contains custom deny list", Objects.requireNonNull(result.getBody()).getPing().getDenyList(), is(equalTo(settings.getPing().getDenyList()))); + assertThat("Response contains custom timeout", Objects.requireNonNull(result.getBody()).getRetrieval().getTimeout(), is(equalTo(settings.getRetrieval().getTimeout().toString()))); + assertThat("Response contains custom rate limit wait", Objects.requireNonNull(result.getBody()).getRetrieval().getRateLimitWait(), is(equalTo(settings.getRetrieval().getRateLimitWait().toString()))); + assertThat("Response indicated non-default settings", Objects.requireNonNull(result.getBody()).getIsDefault(), is(Boolean.FALSE)); + } + + @Test + @DisplayName("HTTP 403: no token") + public void res403_noToken() { + // GIVEN + RequestEntity request = RequestEntity + .get(url()) + .accept(MediaType.APPLICATION_JSON) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("It is forbidden without auth", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + } + + @Test + @DisplayName("HTTP 403: not admin") + public void res403_notAdmin() { + // GIVEN + RequestEntity request = RequestEntity + .get(url()) + .header(HttpHeaders.AUTHORIZATION, NIKOLA_TOKEN) + .accept(MediaType.APPLICATION_JSON) + .build(); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("It is forbidden for non-admin users", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + } +} diff --git a/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_PUT.java b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_PUT.java new file mode 100644 index 000000000..b00ce2255 --- /dev/null +++ b/src/test/java/nl/dtls/fairdatapoint/acceptance/index/settings/List_PUT.java @@ -0,0 +1,273 @@ +/** + * The MIT License + * Copyright © 2017 DTL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package nl.dtls.fairdatapoint.acceptance.index.settings; + +import nl.dtls.fairdatapoint.WebIntegrationTest; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsPingDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsRetrievalDTO; +import nl.dtls.fairdatapoint.api.dto.index.settings.IndexSettingsUpdateDTO; +import nl.dtls.fairdatapoint.database.mongo.repository.IndexSettingsRepository; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettings; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettingsPing; +import nl.dtls.fairdatapoint.entity.index.settings.IndexSettingsRetrieval; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; + +import java.net.URI; +import java.time.Duration; +import java.util.Collections; +import java.util.Objects; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsNull.notNullValue; + +@DisplayName("PUT /index/settings") +public class List_PUT extends WebIntegrationTest { + + @Autowired + private IndexSettingsRepository indexSettingsRepository; + + private final ParameterizedTypeReference responseType = + new ParameterizedTypeReference<>() { + }; + + private URI url() { + return URI.create("/index/settings"); + } + + private IndexSettings customSettings1() { + return new IndexSettings() + .toBuilder() + .ping( + new IndexSettingsPing() + .toBuilder() + .denyList(Collections.singletonList("http://localhost.*$")) + .rateLimitDuration(Duration.ofMinutes(17)) + .validDuration(Duration.ofDays(5)) + .rateLimitHits(666) + .build() + ) + .retrieval( + new IndexSettingsRetrieval() + .toBuilder() + .rateLimitWait(Duration.ofHours(16)) + .timeout(Duration.ofSeconds(55)) + .build() + ) + .build(); + } + + private IndexSettings customSettings2() { + IndexSettings settings = customSettings1(); + settings.getPing().setValidDuration(Duration.ofDays(14)); + settings.getRetrieval().setTimeout(Duration.ofMinutes(2)); + return settings; + } + + private IndexSettingsUpdateDTO customSettingsUpdateDTO() { + IndexSettings customSettings = customSettings1(); + IndexSettingsUpdateDTO dto = new IndexSettingsUpdateDTO(); + IndexSettingsPingDTO pingDTO = new IndexSettingsPingDTO(); + pingDTO.setDenyList(customSettings.getPing().getDenyList()); + pingDTO.setRateLimitDuration(customSettings.getPing().getRateLimitDuration().toString()); + pingDTO.setValidDuration(customSettings.getPing().getValidDuration().toString()); + pingDTO.setRateLimitHits(customSettings.getPing().getRateLimitHits()); + dto.setPing(pingDTO); + IndexSettingsRetrievalDTO retrievalDTO = new IndexSettingsRetrievalDTO(); + retrievalDTO.setRateLimitWait(customSettings.getRetrieval().getRateLimitWait().toString()); + retrievalDTO.setTimeout(customSettings.getRetrieval().getTimeout().toString()); + dto.setRetrieval(retrievalDTO); + return dto; + } + + private IndexSettingsUpdateDTO invalidUpdateDTO1() { + IndexSettingsUpdateDTO dto = customSettingsUpdateDTO(); + dto.getPing().setValidDuration("666"); + return dto; + } + + private IndexSettingsUpdateDTO invalidUpdateDTO2() { + IndexSettingsUpdateDTO dto = customSettingsUpdateDTO(); + dto.getPing().setDenyList(null); + return dto; + } + + @Test + @DisplayName("HTTP 200: update settings from defaults") + public void res200_updateSettingsFromDefaults() { + // GIVEN: prepare data + IndexSettings settings = customSettings1(); + indexSettingsRepository.deleteAll(); + + // AND: prepare request + RequestEntity request = RequestEntity + .put(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(customSettingsUpdateDTO()); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("Settings are created", indexSettingsRepository.findAll().size(), is(equalTo(1))); + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.OK))); + assertThat("Response body is not null", result.getBody(), is(notNullValue())); + assertThat("Response contains default valid duration", Objects.requireNonNull(result.getBody()).getPing().getValidDuration(), is(equalTo(settings.getPing().getValidDuration().toString()))); + assertThat("Response contains default rate limit duration", Objects.requireNonNull(result.getBody()).getPing().getRateLimitDuration(), is(equalTo(settings.getPing().getRateLimitDuration().toString()))); + assertThat("Response contains default rate limit hits", Objects.requireNonNull(result.getBody()).getPing().getRateLimitHits(), is(equalTo(settings.getPing().getRateLimitHits()))); + assertThat("Response contains default deny list", Objects.requireNonNull(result.getBody()).getPing().getDenyList(), is(equalTo(settings.getPing().getDenyList()))); + assertThat("Response contains default timeout", Objects.requireNonNull(result.getBody()).getRetrieval().getTimeout(), is(equalTo(settings.getRetrieval().getTimeout().toString()))); + assertThat("Response contains default rate limit wait", Objects.requireNonNull(result.getBody()).getRetrieval().getRateLimitWait(), is(equalTo(settings.getRetrieval().getRateLimitWait().toString()))); + } + + @Test + @DisplayName("HTTP 200: update settings from custom") + public void res200_updateSettingsFromCustom() { + // GIVEN: prepare data + IndexSettingsUpdateDTO reqDTO = customSettingsUpdateDTO(); + IndexSettings settings = customSettings1(); + indexSettingsRepository.deleteAll(); + indexSettingsRepository.insert(customSettings2()); + + // AND: prepare request + RequestEntity request = RequestEntity + .put(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(reqDTO); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("Settings are created", indexSettingsRepository.findAll().size(), is(equalTo(1))); + assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.OK))); + assertThat("Response body is not null", result.getBody(), is(notNullValue())); + assertThat("Response contains default valid duration", Objects.requireNonNull(result.getBody()).getPing().getValidDuration(), is(equalTo(settings.getPing().getValidDuration().toString()))); + assertThat("Response contains default rate limit duration", Objects.requireNonNull(result.getBody()).getPing().getRateLimitDuration(), is(equalTo(settings.getPing().getRateLimitDuration().toString()))); + assertThat("Response contains default rate limit hits", Objects.requireNonNull(result.getBody()).getPing().getRateLimitHits(), is(equalTo(settings.getPing().getRateLimitHits()))); + assertThat("Response contains default deny list", Objects.requireNonNull(result.getBody()).getPing().getDenyList(), is(equalTo(settings.getPing().getDenyList()))); + assertThat("Response contains default timeout", Objects.requireNonNull(result.getBody()).getRetrieval().getTimeout(), is(equalTo(settings.getRetrieval().getTimeout().toString()))); + assertThat("Response contains default rate limit wait", Objects.requireNonNull(result.getBody()).getRetrieval().getRateLimitWait(), is(equalTo(settings.getRetrieval().getRateLimitWait().toString()))); + } + + @Test + @DisplayName("HTTP 400: invalid duration format") + public void res400_invalidDurationFormat() { + // GIVEN: prepare data + IndexSettingsUpdateDTO reqDTO = invalidUpdateDTO1(); + indexSettingsRepository.deleteAll(); + + // AND: prepare request + RequestEntity request = RequestEntity + .put(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(reqDTO); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("It indicates bad request", result.getStatusCode(), is(equalTo(HttpStatus.BAD_REQUEST))); + assertThat("No settings are created", indexSettingsRepository.findAll().size(), is(equalTo(0))); + } + + @Test + @DisplayName("HTTP 400: invalid list") + public void res400_invalidList() { + // GIVEN: prepare data + IndexSettingsUpdateDTO reqDTO = invalidUpdateDTO2(); + indexSettingsRepository.deleteAll(); + + // AND: prepare request + RequestEntity request = RequestEntity + .put(url()) + .header(HttpHeaders.AUTHORIZATION, ADMIN_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(reqDTO); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("It indicates bad request", result.getStatusCode(), is(equalTo(HttpStatus.BAD_REQUEST))); + assertThat("No settings are created", indexSettingsRepository.findAll().size(), is(equalTo(0))); + } + + @Test + @DisplayName("HTTP 403: no token") + public void res403_noToken() { + // GIVEN: prepare data + IndexSettingsUpdateDTO reqDTO = customSettingsUpdateDTO(); + indexSettingsRepository.deleteAll(); + + // AND: prepare request + RequestEntity request = RequestEntity + .put(url()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(reqDTO); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("It is forbidden without auth", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + assertThat("No settings are created", indexSettingsRepository.findAll().size(), is(equalTo(0))); + } + + @Test + @DisplayName("HTTP 403: not admin") + public void res403_notAdmin() { + // GIVEN: prepare data + IndexSettingsUpdateDTO reqDTO = customSettingsUpdateDTO(); + indexSettingsRepository.deleteAll(); + + // AND: prepare request + RequestEntity request = RequestEntity + .put(url()) + .header(HttpHeaders.AUTHORIZATION, NIKOLA_TOKEN) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(reqDTO); + + // WHEN + ResponseEntity result = client.exchange(request, responseType); + + // THEN + assertThat("It is forbidden for non-admin users", result.getStatusCode(), is(equalTo(HttpStatus.FORBIDDEN))); + assertThat("No settings are created", indexSettingsRepository.findAll().size(), is(equalTo(0))); + } +} diff --git a/src/test/resources/application-testing.yml b/src/test/resources/application-testing.yml index 023d98d40..5a3ca6053 100644 --- a/src/test/resources/application-testing.yml +++ b/src/test/resources/application-testing.yml @@ -1,8 +1,8 @@ instance: - clientUrl: http://localhost:8084 + clientUrl: http://localhost:8088 server: - port: 8084 + port: 8088 spring: data: @@ -14,12 +14,5 @@ security: token: expiration: 9999 - fdp-index: enabled: true - events: - retrieval: - rateLimitWait: PT10M # 10 minutes (ISO 8601) - timeout: PT1M # 1 minute (ISO 8601) - ping: - validDuration: P7D # 7 days (ISO 8601) \ No newline at end of file