diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6da2b6f..23fe5e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,16 @@ 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).
+## [0.3.0]
+
+The final release as the Index has been moved to [FAIR Data Point](https://github.com/FAIRDataTeam/FAIRDataPoint) (in [38a5fbd](https://github.com/FAIRDataTeam/FAIRDataPoint/commit/38a5fbdf3bc988447beda2c5daaa1938f15e5408)).
+
+### Added
+
+- Simple webhooks (from database) with possibility to select specific event(s) and/or
+ specific entries
+- Ability to trigger metadata retrieval using secured API endpoints (admin-only)
+
## [0.2.0]
### Added
@@ -30,7 +40,7 @@ Initial version for simple list of FAIR Data Points.
- REST API to retrieve entries list (both all and paged) documented using Swagger/OpenAPI
- Simple webpage with table to browse entries including sorting and pagination
-[Unreleased]: /../../compare/v0.2.0...develop
[0.1.0]: /../../tree/v0.1.0
[0.1.1]: /../../tree/v0.1.1
[0.2.0]: /../../tree/v0.2.0
+[0.3.0]: /../../tree/v0.3.0
diff --git a/README.md b/README.md
index c3a61fe..3c233de 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,12 @@
# FAIRDataPoint-index
-Index of FAIR Data Points
+*Index of FAIR Data Points*
[![FAIRDataPoint-index CI](https://github.com/FAIRDataTeam/FAIRDataPoint-index/workflows/FAIRDataPoint-index%20CI/badge.svg?branch=master)](https://github.com/FAIRDataTeam/FAIRDataPoint-index/actions)
[![License](https://img.shields.io/github/license/FAIRDataTeam/FAIRDataPoint-index)](LICENSE)
+:exclamation: The project has been moved directly to [FAIR Data Point](https://github.com/FAIRDataTeam/FAIRDataPoint) (in [38a5fbd](https://github.com/FAIRDataTeam/FAIRDataPoint/commit/38a5fbdf3bc988447beda2c5daaa1938f15e5408)) and is discontinued in this repository.
+
# Introduction
The index serves as a registry for [FAIR Data Point](https://github.com/FAIRDataTeam/FAIRDataPoint) deployments.
diff --git a/package-lock.json b/package-lock.json
index 1f2f491..5282fca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -683,9 +683,9 @@
}
},
"lodash": {
- "version": "4.17.15",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
- "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+ "version": "4.17.19",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"loud-rejection": {
"version": "1.6.0",
diff --git a/pom.xml b/pom.xml
index 133e95c..57ef410 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,7 +35,7 @@
solutions.fairdata
fairdatapoint-index
- 0.2.0
+ 0.3.0
2020
@@ -56,6 +56,10 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-security
+
org.springframework.boot
spring-boot-starter-data-mongodb
diff --git a/src/main/java/solutions/fairdata/fdp/index/api/controller/AdminController.java b/src/main/java/solutions/fairdata/fdp/index/api/controller/AdminController.java
new file mode 100644
index 0000000..92218c2
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/api/controller/AdminController.java
@@ -0,0 +1,70 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.api.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import solutions.fairdata.fdp.index.entity.events.Event;
+import solutions.fairdata.fdp.index.service.EventService;
+import solutions.fairdata.fdp.index.service.WebhookService;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/admin")
+public class AdminController {
+ private static final Logger logger = LoggerFactory.getLogger(PingController.class);
+
+ @Autowired
+ private EventService eventService;
+
+ @Autowired
+ private WebhookService webhookService;
+
+ @Operation(hidden = true)
+ @PostMapping("/trigger")
+ @PreAuthorize("hasRole('ADMIN')")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void triggerMetadataRetrieve(@RequestParam(required = false) String clientUrl, HttpServletRequest request) {
+ logger.info("Received ping from {}", request.getRemoteAddr());
+ final Event event = eventService.acceptAdminTrigger(request, clientUrl);
+ webhookService.triggerWebhooks(event);
+ eventService.triggerMetadataRetrieval(event);
+ }
+
+ @Operation(hidden = true)
+ @PostMapping("/ping-webhook")
+ @PreAuthorize("hasRole('ADMIN')")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void webhookPing(@RequestParam(required = true) UUID webhook, HttpServletRequest request) {
+ logger.info("Received webhook {} ping trigger from {}", webhook, request.getRemoteAddr());
+ final Event event = webhookService.handleWebhookPing(request, webhook);
+ webhookService.triggerWebhooks(event);
+ }
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/api/controller/PingController.java b/src/main/java/solutions/fairdata/fdp/index/api/controller/PingController.java
index ccdd316..49f10cb 100644
--- a/src/main/java/solutions/fairdata/fdp/index/api/controller/PingController.java
+++ b/src/main/java/solutions/fairdata/fdp/index/api/controller/PingController.java
@@ -41,6 +41,7 @@
import solutions.fairdata.fdp.index.api.dto.PingDTO;
import solutions.fairdata.fdp.index.entity.events.Event;
import solutions.fairdata.fdp.index.service.EventService;
+import solutions.fairdata.fdp.index.service.WebhookService;
import javax.servlet.http.HttpServletRequest;
@@ -53,6 +54,9 @@ public class PingController {
@Autowired
private EventService eventService;
+ @Autowired
+ private WebhookService webhookService;
+
@Operation(
description = "Inform about running FAIR Data Point. It is expected to send pings regularly (at least weekly). There is a rate limit set both per single IP within a period of time and per URL in message.",
requestBody = @RequestBody(
@@ -80,8 +84,9 @@ public class PingController {
@ResponseStatus(HttpStatus.NO_CONTENT)
public void receivePing(HttpEntity httpEntity, HttpServletRequest request) {
logger.info("Received ping from {}", request.getRemoteAddr());
- final Event incomingPingEvent = eventService.acceptIncomingPing(httpEntity, request);
- logger.info("Triggering metadata retrieval for {}", incomingPingEvent.getRelatedTo().getClientUrl());
- eventService.triggerMetadataRetrieval(incomingPingEvent);
+ final Event event = eventService.acceptIncomingPing(httpEntity, request);
+ logger.info("Triggering metadata retrieval for {}", event.getRelatedTo().getClientUrl());
+ eventService.triggerMetadataRetrieval(event);
+ webhookService.triggerWebhooks(event);
}
}
diff --git a/src/main/java/solutions/fairdata/fdp/index/api/dto/WebhookPayloadDTO.java b/src/main/java/solutions/fairdata/fdp/index/api/dto/WebhookPayloadDTO.java
new file mode 100644
index 0000000..3924d64
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/api/dto/WebhookPayloadDTO.java
@@ -0,0 +1,37 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.api.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import solutions.fairdata.fdp.index.entity.webhooks.WebhookEvent;
+
+@Data
+@NoArgsConstructor
+public class WebhookPayloadDTO {
+ private WebhookEvent event;
+ private String uuid;
+ private String clientUrl;
+ private String timestamp;
+ private String secret;
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/api/filters/TokenFilter.java b/src/main/java/solutions/fairdata/fdp/index/api/filters/TokenFilter.java
new file mode 100644
index 0000000..bb19b10
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/api/filters/TokenFilter.java
@@ -0,0 +1,67 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.api.filters;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import solutions.fairdata.fdp.index.service.TokenService;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Optional;
+
+import static java.util.Optional.of;
+import static java.util.Optional.ofNullable;
+
+@Component
+public class TokenFilter extends OncePerRequestFilter {
+
+ private static final String PREFIX = "Bearer ";
+
+ @Autowired
+ private TokenService tokenService;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+ String token = getToken(request);
+ if (token != null) {
+ Optional auth = tokenService.getAuthentication(token);
+ auth.ifPresent(a -> SecurityContextHolder.getContext().setAuthentication(a));
+ }
+ filterChain.doFilter(request, response);
+ }
+
+ private String getToken(HttpServletRequest request) {
+ return ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
+ .filter(h -> h.startsWith(PREFIX))
+ .flatMap(h -> of(h.substring(PREFIX.length())))
+ .orElse(null);
+ }
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/api/filters/package-info.java b/src/main/java/solutions/fairdata/fdp/index/api/filters/package-info.java
new file mode 100644
index 0000000..3346506
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/api/filters/package-info.java
@@ -0,0 +1,25 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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.
+ */
+
+@javax.annotation.ParametersAreNonnullByDefault
+package solutions.fairdata.fdp.index.api.filters;
diff --git a/src/main/java/solutions/fairdata/fdp/index/config/SecurityConfig.java b/src/main/java/solutions/fairdata/fdp/index/config/SecurityConfig.java
new file mode 100644
index 0000000..03c121a
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/config/SecurityConfig.java
@@ -0,0 +1,56 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import solutions.fairdata.fdp.index.api.filters.TokenFilter;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ TokenFilter tokenFilter;
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .httpBasic().disable()
+ .csrf().disable()
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and()
+ .authorizeRequests()
+ .antMatchers("/admin**").authenticated()
+ .anyRequest().permitAll()
+ .and()
+ .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
+ }
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/database/repository/EventRepository.java b/src/main/java/solutions/fairdata/fdp/index/database/repository/EventRepository.java
index e7c41c3..62e5a04 100644
--- a/src/main/java/solutions/fairdata/fdp/index/database/repository/EventRepository.java
+++ b/src/main/java/solutions/fairdata/fdp/index/database/repository/EventRepository.java
@@ -27,13 +27,16 @@
import org.springframework.data.mongodb.repository.MongoRepository;
import solutions.fairdata.fdp.index.entity.IndexEntry;
import solutions.fairdata.fdp.index.entity.events.Event;
+import solutions.fairdata.fdp.index.entity.events.EventType;
import java.time.Instant;
import java.util.List;
public interface EventRepository extends MongoRepository {
- Iterable getAllByFinishedIsNull();
+ List getAllByType(EventType type);
+
+ List getAllByFinishedIsNull();
Page getAllByRelatedTo(IndexEntry indexEntry, Pageable pageable);
diff --git a/src/main/java/solutions/fairdata/fdp/index/database/repository/TokenRepository.java b/src/main/java/solutions/fairdata/fdp/index/database/repository/TokenRepository.java
new file mode 100644
index 0000000..04ab8a6
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/database/repository/TokenRepository.java
@@ -0,0 +1,32 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.database.repository;
+
+import org.springframework.data.mongodb.repository.MongoRepository;
+import solutions.fairdata.fdp.index.entity.Token;
+
+import java.util.Optional;
+
+public interface TokenRepository extends MongoRepository {
+ Optional findByToken(String token);
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/database/repository/WebhookRepository.java b/src/main/java/solutions/fairdata/fdp/index/database/repository/WebhookRepository.java
new file mode 100644
index 0000000..16f75f8
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/database/repository/WebhookRepository.java
@@ -0,0 +1,33 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.database.repository;
+
+import org.springframework.data.mongodb.repository.MongoRepository;
+import solutions.fairdata.fdp.index.entity.webhooks.Webhook;
+
+import java.util.Optional;
+import java.util.UUID;
+
+public interface WebhookRepository extends MongoRepository {
+ Optional findByUuid(UUID uuid);
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/Token.java b/src/main/java/solutions/fairdata/fdp/index/entity/Token.java
new file mode 100644
index 0000000..221c35f
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/Token.java
@@ -0,0 +1,47 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.entity;
+
+import lombok.Data;
+import org.bson.types.ObjectId;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Document
+@Data
+public class Token {
+ @Id
+ protected ObjectId id;
+ @Indexed(unique=true)
+ private String token;
+ @NotNull
+ private String name;
+ @NotNull
+ private List roles;
+ @NotNull
+ private String note;
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/events/AdminTrigger.java b/src/main/java/solutions/fairdata/fdp/index/entity/events/AdminTrigger.java
new file mode 100644
index 0000000..8b2a9f8
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/events/AdminTrigger.java
@@ -0,0 +1,36 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.entity.events;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AdminTrigger {
+ private String remoteAddr;
+ private String tokenName;
+ private String clientUrl;
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/events/Event.java b/src/main/java/solutions/fairdata/fdp/index/entity/events/Event.java
index 5648fe5..1cf3d8a 100644
--- a/src/main/java/solutions/fairdata/fdp/index/entity/events/Event.java
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/events/Event.java
@@ -60,6 +60,9 @@ public class Event {
// Content (one of those)
private IncomingPing incomingPing;
private MetadataRetrieval metadataRetrieval;
+ private AdminTrigger adminTrigger;
+ private WebhookPing webhookPing;
+ private WebhookTrigger webhookTrigger;
@NotNull
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@@ -100,4 +103,24 @@ public Event(Integer version, Event triggerEvent, IndexEntry relatedTo, Metadata
this.relatedTo = relatedTo;
this.metadataRetrieval = metadataRetrieval;
}
+
+ public Event(Integer version, AdminTrigger adminTrigger) {
+ this.type = EventType.AdminTrigger;
+ this.version = version;
+ this.adminTrigger = adminTrigger;
+ }
+
+ public Event(Integer version, WebhookTrigger webhookTrigger, Event triggerEvent) {
+ this.type = EventType.WebhookTrigger;
+ this.version = version;
+ this.webhookTrigger = webhookTrigger;
+ this.triggeredBy = triggerEvent;
+ this.relatedTo = triggerEvent.getRelatedTo();
+ }
+
+ public Event(Integer version, WebhookPing webhookPing) {
+ this.type = EventType.WebhookPing;
+ this.version = version;
+ this.webhookPing = webhookPing;
+ }
}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/events/EventType.java b/src/main/java/solutions/fairdata/fdp/index/entity/events/EventType.java
index c9574cc..b575abd 100644
--- a/src/main/java/solutions/fairdata/fdp/index/entity/events/EventType.java
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/events/EventType.java
@@ -23,6 +23,9 @@
package solutions.fairdata.fdp.index.entity.events;
public enum EventType {
+ AdminTrigger,
MetadataRetrieval,
- IncomingPing;
+ WebhookTrigger,
+ IncomingPing,
+ WebhookPing,
}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/events/IncomingPing.java b/src/main/java/solutions/fairdata/fdp/index/entity/events/IncomingPing.java
index 5dfe1e4..2398d43 100644
--- a/src/main/java/solutions/fairdata/fdp/index/entity/events/IncomingPing.java
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/events/IncomingPing.java
@@ -32,4 +32,5 @@
@AllArgsConstructor
public class IncomingPing {
private Exchange exchange;
+ private Boolean newEntry;
}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/events/WebhookPing.java b/src/main/java/solutions/fairdata/fdp/index/entity/events/WebhookPing.java
new file mode 100644
index 0000000..e7287a9
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/events/WebhookPing.java
@@ -0,0 +1,38 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.entity.events;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class WebhookPing {
+ private String remoteAddr;
+ private String tokenName;
+ private UUID webhookUuid;
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/events/WebhookTrigger.java b/src/main/java/solutions/fairdata/fdp/index/entity/events/WebhookTrigger.java
new file mode 100644
index 0000000..127d47a
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/events/WebhookTrigger.java
@@ -0,0 +1,43 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.entity.events;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.mongodb.core.mapping.DBRef;
+import solutions.fairdata.fdp.index.entity.http.Exchange;
+import solutions.fairdata.fdp.index.entity.webhooks.Webhook;
+import solutions.fairdata.fdp.index.entity.webhooks.WebhookEvent;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class WebhookTrigger {
+ @DBRef
+ private Webhook webhook;
+
+ private WebhookEvent matchedEvent;
+
+ private Exchange exchange;
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/Webhook.java b/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/Webhook.java
new file mode 100644
index 0000000..da56a7c
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/Webhook.java
@@ -0,0 +1,63 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.entity.webhooks;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.types.ObjectId;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import javax.validation.constraints.NotNull;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Document(collection = "webhook")
+public class Webhook {
+ @Id
+ protected ObjectId id;
+
+ @Indexed(unique=true)
+ @NotNull
+ private UUID uuid = UUID.randomUUID();
+
+ private String payloadUrl;
+
+ private String secret;
+
+ private boolean allEvents;
+
+ private List events = new ArrayList<>();
+
+ private boolean allEntries;
+
+ private List entries = new ArrayList<>();
+
+ private boolean enabled;
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/WebhookEvent.java b/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/WebhookEvent.java
new file mode 100644
index 0000000..82765c7
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/WebhookEvent.java
@@ -0,0 +1,33 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.entity.webhooks;
+
+public enum WebhookEvent {
+ NewEntry,
+ IncomingPing,
+ EntryValid,
+ EntryInvalid,
+ EntryUnreachable,
+ AdminTrigger,
+ WebhookPing
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/package-info.java b/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/package-info.java
new file mode 100644
index 0000000..6139282
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/entity/webhooks/package-info.java
@@ -0,0 +1,25 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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.
+ */
+
+@javax.annotation.ParametersAreNonnullByDefault
+package solutions.fairdata.fdp.index.entity.webhooks;
diff --git a/src/main/java/solutions/fairdata/fdp/index/exceptions/IndexException.java b/src/main/java/solutions/fairdata/fdp/index/exceptions/IndexException.java
index 3255e4e..b7f227a 100644
--- a/src/main/java/solutions/fairdata/fdp/index/exceptions/IndexException.java
+++ b/src/main/java/solutions/fairdata/fdp/index/exceptions/IndexException.java
@@ -25,7 +25,7 @@
import org.springframework.http.HttpStatus;
import solutions.fairdata.fdp.index.api.dto.ErrorDTO;
-public abstract class IndexException extends Exception {
+public abstract class IndexException extends RuntimeException {
protected final HttpStatus status;
diff --git a/src/main/java/solutions/fairdata/fdp/index/exceptions/NotFoundException.java b/src/main/java/solutions/fairdata/fdp/index/exceptions/NotFoundException.java
new file mode 100644
index 0000000..bc73be5
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/exceptions/NotFoundException.java
@@ -0,0 +1,32 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.exceptions;
+
+import org.springframework.http.HttpStatus;
+
+public class NotFoundException extends IndexException {
+
+ public NotFoundException(String message) {
+ super(message, HttpStatus.NOT_FOUND);
+ }
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/service/EventService.java b/src/main/java/solutions/fairdata/fdp/index/service/EventService.java
index 41e48da..96bb535 100644
--- a/src/main/java/solutions/fairdata/fdp/index/service/EventService.java
+++ b/src/main/java/solutions/fairdata/fdp/index/service/EventService.java
@@ -33,6 +33,8 @@
import org.springframework.http.HttpEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import solutions.fairdata.fdp.index.api.dto.PingDTO;
import solutions.fairdata.fdp.index.database.repository.EventRepository;
@@ -45,13 +47,16 @@
import solutions.fairdata.fdp.index.entity.http.Exchange;
import solutions.fairdata.fdp.index.entity.http.ExchangeState;
import solutions.fairdata.fdp.index.exceptions.IncorrectPingFormatException;
+import solutions.fairdata.fdp.index.exceptions.NotFoundException;
import solutions.fairdata.fdp.index.exceptions.RateLimitException;
+import solutions.fairdata.fdp.index.utils.AdminTriggerUtils;
import solutions.fairdata.fdp.index.utils.IncomingPingUtils;
import solutions.fairdata.fdp.index.utils.MetadataRetrievalUtils;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.time.Instant;
+import java.util.Optional;
@Service
public class EventService {
@@ -72,6 +77,9 @@ public class EventService {
@Autowired
private IndexEntryService indexEntryService;
+ @Autowired
+ private WebhookService webhookService;
+
@Autowired
private EventsConfig eventsConfig;
@@ -90,7 +98,7 @@ public Event acceptIncomingPing(HttpEntity httpEntity, HttpServletReques
var rateLimitSince = Instant.now().minus(eventsConfig.getPingRateLimitDuration());
var previousPings = eventRepository.findAllByIncomingPingExchangeRemoteAddrAndCreatedAfter(remoteAddr, rateLimitSince);
if (previousPings.size() > eventsConfig.getPingRateLimitHits()) {
- logger.warn("Rate limit for PING reached by: " + remoteAddr);
+ 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())
@@ -103,6 +111,7 @@ public Event acceptIncomingPing(HttpEntity httpEntity, HttpServletReques
try {
var pingDTO = objectMapper.readValue(httpEntity.getBody(), PingDTO.class);
var indexEntry = indexEntryService.storeEntry(pingDTO);
+ event.getIncomingPing().setNewEntry(indexEntry.getRegistrationTime().equals(indexEntry.getModificationTime()));
event.getIncomingPing().getExchange().getResponse().setCode(204);
event.setRelatedTo(indexEntry);
logger.info("Accepted incoming ping as a new event");
@@ -112,7 +121,7 @@ public Event acceptIncomingPing(HttpEntity httpEntity, HttpServletReques
event.getIncomingPing().getExchange().getResponse().setBody(objectMapper.writeValueAsString(ex.getErrorDTO()));
event.setFinished(Instant.now());
eventRepository.save(event);
- logger.info("Incoming ping has incorrect format: " + e.getMessage());
+ logger.info("Incoming ping has incorrect format: {}", e.getMessage());
throw ex;
}
event.setFinished(Instant.now());
@@ -126,67 +135,74 @@ private void processMetadataRetrieval(Event event) {
eventRepository.save(event);
event.execute();
- logger.info("Retrieving metadata for " + clientUrl);
+ logger.info("Retrieving metadata for {}", clientUrl);
MetadataRetrievalUtils.retrieveRepositoryMetadata(event, eventsConfig.getRetrievalTimeout());
Exchange ex = event.getMetadataRetrieval().getExchange();
if (ex.getState() == ExchangeState.Retrieved) {
try {
- logger.info("Parsing metadata for " + clientUrl);
+ logger.info("Parsing metadata for {}", clientUrl);
var metadata = MetadataRetrievalUtils.parseRepositoryMetadata(ex.getResponse().getBody());
if (metadata.isPresent()) {
event.getMetadataRetrieval().setMetadata(metadata.get());
event.getRelatedTo().setCurrentMetadata(metadata.get());
event.getRelatedTo().setState(IndexEntryState.Valid);
- logger.info("Storing metadata for " + clientUrl);
+ logger.info("Storing metadata for {}", clientUrl);
indexEntryRepository.save(event.getRelatedTo());
} else {
- logger.info("Repository not found in metadata for " + clientUrl);
+ logger.info("Repository not found in metadata for {}", clientUrl);
event.getRelatedTo().setState(IndexEntryState.Invalid);
event.getMetadataRetrieval().setError("Repository not found in metadata");
}
} catch (Exception e) {
- logger.info("Cannot parse metadata for " + clientUrl);
+ logger.info("Cannot parse metadata for {}", clientUrl);
event.getRelatedTo().setState(IndexEntryState.Invalid);
event.getMetadataRetrieval().setError("Cannot parse metadata");
}
} else {
event.getRelatedTo().setState(IndexEntryState.Unreachable);
- logger.info("Cannot retrieve metadata for " + clientUrl + ": " + ex.getError());
+ logger.info("Cannot retrieve metadata for {}: {}", clientUrl, ex.getError());
}
} else {
- logger.info("Rate limit reached for " + clientUrl + " (skipping metadata retrieval)");
+ logger.info("Rate limit reached for {} (skipping metadata retrieval)", clientUrl);
event.getMetadataRetrieval().setError("Rate limit reached (skipping)");
}
event.getRelatedTo().setLastRetrievalTime(Instant.now());
event.finish();
- eventRepository.save(event);
+ event = eventRepository.save(event);
indexEntryRepository.save(event.getRelatedTo());
+ webhookService.triggerWebhooks(event);
}
@Async
public void triggerMetadataRetrieval(Event triggerEvent) {
- var event = MetadataRetrievalUtils.prepareEvent(triggerEvent);
- logger.info("Triggering metadata retrieval for " + triggerEvent.getRelatedTo().getClientUrl());
- try {
- processMetadataRetrieval(event);
- } catch (Exception e) {
- logger.error("Failed to retrieve metadata: " + e.getMessage());
+ logger.info("Initiating metadata retrieval triggered by {}", triggerEvent.getUuid());
+ Iterable events = MetadataRetrievalUtils.prepareEvents(triggerEvent, indexEntryService);
+ for (Event event: events) {
+ logger.info("Triggering metadata retrieval for {} as {}", event.getRelatedTo().getClientUrl(), event.getUuid());
+ try {
+ processMetadataRetrieval(event);
+ } catch (Exception e) {
+ logger.error("Failed to retrieve metadata: {}", e.getMessage());
+ }
}
+ logger.info("Finished metadata retrieval triggered by {}", triggerEvent.getUuid());
}
private void resumeUnfinishedEvents() {
logger.info("Resuming unfinished events");
for (Event event : eventRepository.getAllByFinishedIsNull()) {
- logger.info("Resuming event " + event.getUuid());
+ logger.info("Resuming event {}", event.getUuid());
try {
if (event.getType() == EventType.MetadataRetrieval) {
processMetadataRetrieval(event);
+ } else if (event.getType() == EventType.WebhookTrigger) {
+ webhookService.processWebhookTrigger(event);
} else {
- logger.warn("Unknown event type " + event.getUuid());
+ logger.warn("Unknown event type {} ({})", event.getUuid(), event.getType());
}
} catch (Exception e) {
- logger.error("Failed to resume event " + event.getUuid() + ": " + e.getMessage());
+ logger.error("Failed to resume event {}: {}", event.getUuid(), e.getMessage());
}
}
logger.info("Finished unfinished events");
@@ -196,4 +212,18 @@ private void resumeUnfinishedEvents() {
public void startResumeUnfinishedEvents() {
executor.submit(this::resumeUnfinishedEvents);
}
+
+ public Event acceptAdminTrigger(HttpServletRequest request, String clientUrl) {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ Event event = AdminTriggerUtils.prepareEvent(request, authentication, clientUrl);
+ if (clientUrl != null) {
+ Optional entry = indexEntryService.findEntry(clientUrl);
+ if (entry.isEmpty()) {
+ throw new NotFoundException("There is no such entry: " + clientUrl);
+ }
+ event.setRelatedTo(entry.get());
+ }
+ event.finish();
+ return eventRepository.save(event);
+ }
}
diff --git a/src/main/java/solutions/fairdata/fdp/index/service/TokenService.java b/src/main/java/solutions/fairdata/fdp/index/service/TokenService.java
new file mode 100644
index 0000000..1d4c218
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/service/TokenService.java
@@ -0,0 +1,54 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Service;
+import solutions.fairdata.fdp.index.database.repository.TokenRepository;
+import solutions.fairdata.fdp.index.entity.Token;
+
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class TokenService {
+
+ @Autowired
+ private TokenRepository tokenRepository;
+
+ public Optional getAuthentication(String token) {
+ // Currently just verify presence of token in DB,
+ // in the future there might be some permissions
+ return tokenRepository.findByToken(token).map(this::toAuthentication);
+ }
+
+ private Authentication toAuthentication(Token token) {
+ return new UsernamePasswordAuthenticationToken(
+ token.getName(), token.getToken(),
+ token.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())
+ );
+ }
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/service/WebhookService.java b/src/main/java/solutions/fairdata/fdp/index/service/WebhookService.java
new file mode 100644
index 0000000..6ff557f
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/service/WebhookService.java
@@ -0,0 +1,143 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+import solutions.fairdata.fdp.index.api.dto.WebhookPayloadDTO;
+import solutions.fairdata.fdp.index.database.repository.EventRepository;
+import solutions.fairdata.fdp.index.database.repository.WebhookRepository;
+import solutions.fairdata.fdp.index.entity.config.EventsConfig;
+import solutions.fairdata.fdp.index.entity.events.Event;
+import solutions.fairdata.fdp.index.entity.webhooks.Webhook;
+import solutions.fairdata.fdp.index.entity.webhooks.WebhookEvent;
+import solutions.fairdata.fdp.index.exceptions.NotFoundException;
+import solutions.fairdata.fdp.index.utils.WebhookUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Optional;
+import java.util.UUID;
+
+
+@Service
+public class WebhookService {
+ private static final Logger logger = LoggerFactory.getLogger(WebhookService.class);
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ WebhookRepository webhookRepository;
+
+ @Autowired
+ EventRepository eventRepository;
+
+ @Autowired
+ private EventsConfig eventsConfig;
+
+ private static final String SECRET_PLACEHOLDER = "*** HIDDEN ***";
+
+ public void processWebhookTrigger(Event event) {
+ event.execute();
+ eventRepository.save(event);
+ WebhookPayloadDTO webhookPayload = WebhookUtils.preparePayload(event);
+ try {
+ String payloadWithSecret = objectMapper.writeValueAsString(webhookPayload);
+ String signature = WebhookUtils.computeHashSignature(payloadWithSecret);
+ webhookPayload.setSecret(SECRET_PLACEHOLDER);
+ String payloadWithoutSecret = objectMapper.writeValueAsString(webhookPayload);
+ WebhookUtils.postWebhook(event, eventsConfig.getRetrievalTimeout(), payloadWithoutSecret, signature);
+ } catch (JsonProcessingException e) {
+ logger.error("Failed to convert webhook payload to string");
+ } catch (NoSuchAlgorithmException e) {
+ logger.error("Could not compute SHA-1 signature of payload");
+ }
+ event.finish();
+ eventRepository.save(event);
+ }
+
+ @Async
+ public void triggerWebhook(Webhook webhook, WebhookEvent webhookEvent, Event triggerEvent) {
+ Event event = WebhookUtils.prepareTriggerEvent(webhook, webhookEvent, triggerEvent);
+ processWebhookTrigger(event);
+ }
+
+ @Async
+ public void triggerWebhooks(WebhookEvent webhookEvent, Event triggerEvent) {
+ logger.info("Triggered webhook event {} by event {}", webhookEvent, triggerEvent.getUuid());
+ WebhookUtils.filterMatching(webhookRepository.findAll(), webhookEvent, triggerEvent).forEach(webhook -> triggerWebhook(webhook, webhookEvent, triggerEvent));
+ }
+
+ public Event handleWebhookPing(HttpServletRequest request, UUID webhookUuid) {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ Optional webhook = webhookRepository.findByUuid(webhookUuid);
+ Event event = eventRepository.save(WebhookUtils.preparePingEvent(request, authentication, webhookUuid));
+ if (webhook.isEmpty()) {
+ throw new NotFoundException("There is no such webhook: " + webhookUuid);
+ }
+ return event;
+ }
+
+ @Async
+ public void triggerWebhooks(Event triggerEvent) {
+ switch (triggerEvent.getType()) {
+ case AdminTrigger:
+ triggerWebhooks(WebhookEvent.AdminTrigger, triggerEvent);
+ break;
+ case IncomingPing:
+ triggerWebhooks(WebhookEvent.IncomingPing, triggerEvent);
+ if (triggerEvent.getIncomingPing().getNewEntry()) {
+ triggerWebhooks(WebhookEvent.NewEntry, 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());
+ }
+ break;
+ case WebhookPing:
+ triggerWebhooks(WebhookEvent.WebhookPing, triggerEvent);
+ break;
+ default:
+ logger.warn("Invalid event type for webhook trigger: {}", triggerEvent.getType());
+ }
+ }
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/utils/AdminTriggerUtils.java b/src/main/java/solutions/fairdata/fdp/index/utils/AdminTriggerUtils.java
new file mode 100644
index 0000000..8cbfd50
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/utils/AdminTriggerUtils.java
@@ -0,0 +1,42 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.utils;
+
+import org.springframework.security.core.Authentication;
+import solutions.fairdata.fdp.index.entity.events.AdminTrigger;
+import solutions.fairdata.fdp.index.entity.events.Event;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class AdminTriggerUtils {
+
+ private static final Integer VERSION = 1;
+
+ public static Event prepareEvent(HttpServletRequest request, Authentication authentication, String clientUrl) {
+ var adminTrigger = new AdminTrigger();
+ adminTrigger.setRemoteAddr(request.getRemoteAddr());
+ adminTrigger.setTokenName(authentication.getName());
+ adminTrigger.setClientUrl(clientUrl);
+ return new Event(VERSION, adminTrigger);
+ }
+}
diff --git a/src/main/java/solutions/fairdata/fdp/index/utils/MetadataRetrievalUtils.java b/src/main/java/solutions/fairdata/fdp/index/utils/MetadataRetrievalUtils.java
index dfdabe1..f81c810 100644
--- a/src/main/java/solutions/fairdata/fdp/index/utils/MetadataRetrievalUtils.java
+++ b/src/main/java/solutions/fairdata/fdp/index/utils/MetadataRetrievalUtils.java
@@ -42,6 +42,7 @@
import solutions.fairdata.fdp.index.entity.http.Exchange;
import solutions.fairdata.fdp.index.entity.http.ExchangeDirection;
import solutions.fairdata.fdp.index.entity.http.ExchangeState;
+import solutions.fairdata.fdp.index.service.IndexEntryService;
import java.io.IOException;
import java.io.StringReader;
@@ -52,7 +53,9 @@
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Optional;
public class MetadataRetrievalUtils {
@@ -88,8 +91,20 @@ public static boolean shouldRetrieve(Event triggerEvent, Duration rateLimitWait)
return Duration.between(lastRetrieval, Instant.now()).compareTo(rateLimitWait) > 0;
}
- public static Event prepareEvent(Event triggerEvent) {
- return new Event(VERSION, triggerEvent, triggerEvent.getRelatedTo(), new MetadataRetrieval());
+ public static Iterable prepareEvents(Event triggerEvent, IndexEntryService indexEntryService) {
+ ArrayList events = new ArrayList<>();
+ if (triggerEvent.getType() == EventType.IncomingPing) {
+ events.add(new Event(VERSION, triggerEvent, triggerEvent.getRelatedTo(), new MetadataRetrieval()));
+ } else if (triggerEvent.getType() == EventType.AdminTrigger) {
+ if (triggerEvent.getAdminTrigger().getClientUrl() == null) {
+ indexEntryService.getAllEntries().forEach(
+ entry -> events.add(new Event(VERSION, triggerEvent, entry, new MetadataRetrieval()))
+ );
+ } else {
+ events.add(new Event(VERSION, triggerEvent, triggerEvent.getRelatedTo(), new MetadataRetrieval()));
+ }
+ }
+ return events;
}
public static void retrieveRepositoryMetadata(Event event, Duration timeout) {
diff --git a/src/main/java/solutions/fairdata/fdp/index/utils/WebhookUtils.java b/src/main/java/solutions/fairdata/fdp/index/utils/WebhookUtils.java
new file mode 100644
index 0000000..8010727
--- /dev/null
+++ b/src/main/java/solutions/fairdata/fdp/index/utils/WebhookUtils.java
@@ -0,0 +1,132 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.utils;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+import solutions.fairdata.fdp.index.api.dto.WebhookPayloadDTO;
+import solutions.fairdata.fdp.index.entity.events.Event;
+import solutions.fairdata.fdp.index.entity.events.WebhookPing;
+import solutions.fairdata.fdp.index.entity.events.WebhookTrigger;
+import solutions.fairdata.fdp.index.entity.http.Exchange;
+import solutions.fairdata.fdp.index.entity.http.ExchangeDirection;
+import solutions.fairdata.fdp.index.entity.http.ExchangeState;
+import solutions.fairdata.fdp.index.entity.webhooks.Webhook;
+import solutions.fairdata.fdp.index.entity.webhooks.WebhookEvent;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+public class WebhookUtils {
+
+ private static final Integer VERSION = 1;
+
+ private static final HttpClient client = HttpClient.newBuilder()
+ .version(HttpClient.Version.HTTP_2)
+ .followRedirects(HttpClient.Redirect.ALWAYS)
+ .build();
+
+ private static boolean webhookMatches(Webhook webhook, WebhookEvent webhookEvent, Event triggerEvent) {
+ boolean matchEvent = webhook.isAllEvents() || webhook.getEvents().contains(webhookEvent);
+ boolean matchEntry = webhook.isAllEntries() || triggerEvent.getRelatedTo() == null || webhook.getEntries().contains(triggerEvent.getRelatedTo().getClientUrl());
+ return matchEvent && matchEntry && webhook.isEnabled();
+ }
+
+ public static Stream filterMatching(List webhooks, WebhookEvent webhookEvent, Event triggerEvent) {
+ return webhooks.parallelStream().filter(webhook -> WebhookUtils.webhookMatches(webhook, webhookEvent, triggerEvent));
+ }
+
+ public static Event prepareTriggerEvent(Webhook webhook, WebhookEvent webhookEvent, Event triggerEvent) {
+ var webhookTrigger = new WebhookTrigger();
+ webhookTrigger.setWebhook(webhook);
+ webhookTrigger.setMatchedEvent(webhookEvent);
+ return new Event(VERSION, webhookTrigger, triggerEvent);
+ }
+
+ public static Event preparePingEvent(HttpServletRequest request, Authentication authentication, UUID webhookUuid) {
+ var webhookPing = new WebhookPing();
+ webhookPing.setWebhookUuid(webhookUuid);
+ webhookPing.setRemoteAddr(request.getRemoteAddr());
+ webhookPing.setTokenName(authentication.getName());
+ return new Event(VERSION, webhookPing);
+ }
+
+ public static WebhookPayloadDTO preparePayload(Event event) {
+ WebhookPayloadDTO webhookPayload = new WebhookPayloadDTO();
+ webhookPayload.setEvent(event.getWebhookTrigger().getMatchedEvent());
+ webhookPayload.setClientUrl(event.getRelatedTo().getClientUrl());
+ webhookPayload.setSecret(event.getWebhookTrigger().getWebhook().getSecret());
+ webhookPayload.setUuid(event.getUuid().toString());
+ webhookPayload.setTimestamp(Instant.now().toString());
+ return webhookPayload;
+ }
+
+ public static String computeHashSignature(String value) throws NoSuchAlgorithmException {
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ digest.update(value.getBytes(StandardCharsets.UTF_8));
+ return String.format("sha1=%040x", new BigInteger(1, digest.digest()));
+ }
+
+ public static void postWebhook(Event event, Duration timeout, String payload, String signature) {
+ var ex = new Exchange(ExchangeDirection.OUTGOING);
+ event.getWebhookTrigger().setExchange(ex);
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(event.getWebhookTrigger().getWebhook().getPayloadUrl()))
+ .timeout(timeout)
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString())
+ .header("X-Signature", signature)
+ .POST(HttpRequest.BodyPublishers.ofString(payload))
+ .build();
+ ex.getRequest().setFromHttpRequest(request);
+ ex.setState(ExchangeState.Requested);
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ ex.getResponse().setFromHttpResponse(response);
+ ex.setState(ExchangeState.Retrieved);
+ } catch (InterruptedException e) {
+ ex.setState(ExchangeState.Timeout);
+ ex.setError("Timeout");
+ } catch (IllegalArgumentException e) {
+ ex.setState(ExchangeState.Failed);
+ ex.setError("Invalid URI: " + e.getMessage());
+ } catch (IOException e) {
+ ex.setState(ExchangeState.Failed);
+ ex.setError("IO error: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/solutions/fairdata/fdp/index/WebIntegrationTest.java b/src/test/java/solutions/fairdata/fdp/index/WebIntegrationTest.java
index fadbb65..0fbc071 100644
--- a/src/test/java/solutions/fairdata/fdp/index/WebIntegrationTest.java
+++ b/src/test/java/solutions/fairdata/fdp/index/WebIntegrationTest.java
@@ -23,7 +23,6 @@
package solutions.fairdata.fdp.index;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
diff --git a/src/test/java/solutions/fairdata/fdp/index/acceptance/api/admin/Trigger_POST_Test.java b/src/test/java/solutions/fairdata/fdp/index/acceptance/api/admin/Trigger_POST_Test.java
new file mode 100644
index 0000000..53c670b
--- /dev/null
+++ b/src/test/java/solutions/fairdata/fdp/index/acceptance/api/admin/Trigger_POST_Test.java
@@ -0,0 +1,210 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.acceptance.api.admin;
+
+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.data.mongodb.core.MongoTemplate;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import solutions.fairdata.fdp.index.WebIntegrationTest;
+import solutions.fairdata.fdp.index.database.repository.EventRepository;
+import solutions.fairdata.fdp.index.database.repository.IndexEntryRepository;
+import solutions.fairdata.fdp.index.database.repository.TokenRepository;
+import solutions.fairdata.fdp.index.entity.IndexEntry;
+import solutions.fairdata.fdp.index.entity.Token;
+import solutions.fairdata.fdp.index.entity.events.Event;
+import solutions.fairdata.fdp.index.entity.events.EventType;
+import solutions.fairdata.fdp.index.fixtures.IndexEntryFixtures;
+import solutions.fairdata.fdp.index.fixtures.TokenFixtures;
+
+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 /admin/trigger")
+public class Trigger_POST_Test extends WebIntegrationTest {
+
+ @Autowired
+ private EventRepository eventRepository;
+ @Autowired
+ private TokenRepository tokenRepository;
+ @Autowired
+ private IndexEntryRepository indexEntryRepository;
+ @Autowired
+ private MongoTemplate mongoTemplate;
+
+ private final ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {};
+
+ private URI url() {
+ return URI.create("/admin/trigger");
+ }
+
+ private URI url(String clientUrl) {
+ return URI.create("/admin/trigger?clientUrl=" + clientUrl);
+ }
+
+ @Test
+ @DisplayName("HTTP 403: no token")
+ public void res403_noToken() {
+ // GIVEN (prepare data)
+ String clientUrl = "http://example.com";
+ mongoTemplate.getDb().drop();
+
+ // AND (prepare request)
+ RequestEntity request = RequestEntity
+ .post(url(clientUrl))
+ .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 (prepare data)
+ String clientUrl = "http://example.com";
+ Token token = TokenFixtures.adminToken();
+ mongoTemplate.getDb().drop();
+ tokenRepository.save(token);
+
+ // AND (prepare request)
+ RequestEntity request = RequestEntity
+ .post(url(clientUrl))
+ .header(HttpHeaders.AUTHORIZATION, "Bearer myIncorrectToken321")
+ .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 (prepare data)
+ String clientUrl = "http://example.com";
+ Token token = TokenFixtures.noRoleToken();
+ mongoTemplate.getDb().drop();
+ tokenRepository.save(token);
+
+ // AND (prepare request)
+ RequestEntity request = RequestEntity
+ .post(url(clientUrl))
+ .header(HttpHeaders.AUTHORIZATION, "Bearer myIncorrectToken321")
+ .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 one")
+ public void res204_triggerOne() {
+ // GIVEN (prepare data)
+ IndexEntry entry = IndexEntryFixtures.entryExample();
+ Token token = TokenFixtures.adminToken();
+ mongoTemplate.getDb().drop();
+ indexEntryRepository.save(entry);
+ tokenRepository.save(token);
+
+ // AND (prepare request)
+ RequestEntity request = RequestEntity
+ .post(url(entry.getClientUrl()))
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + token.getToken())
+ .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 token name", events.get(0).getAdminTrigger().getTokenName(), is(equalTo(token.getName())));
+ assertThat("Records correct client URL", events.get(0).getAdminTrigger().getClientUrl(), is(equalTo(entry.getClientUrl())));
+ }
+
+ @Test
+ @DisplayName("HTTP 204: trigger all")
+ public void res204_triggerAll() {
+ // GIVEN (prepare data)
+ Token token = TokenFixtures.adminToken();
+ mongoTemplate.getDb().drop();
+ tokenRepository.save(token);
+
+ // AND (prepare request)
+ RequestEntity request = RequestEntity
+ .post(url())
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + token.getToken())
+ .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 token name", events.get(0).getAdminTrigger().getTokenName(), is(equalTo(token.getName())));
+ assertThat("Records correct client URL as null", events.get(0).getAdminTrigger().getClientUrl(), is(equalTo(null)));
+ }
+
+ @Test
+ @DisplayName("HTTP 404: trigger non-existing")
+ public void res404_triggerOne() {
+ // GIVEN (prepare data)
+ IndexEntry entry = IndexEntryFixtures.entryExample();
+ Token token = TokenFixtures.adminToken();
+ mongoTemplate.getDb().drop();
+ tokenRepository.save(token);
+
+ // AND (prepare request)
+ RequestEntity request = RequestEntity
+ .post(url(entry.getClientUrl()))
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + token.getToken())
+ .build();
+
+ // WHEN
+ ResponseEntity result = client.exchange(request, responseType);
+
+ // THEN
+ assertThat("Correct response code is received", result.getStatusCode(), is(equalTo(HttpStatus.NOT_FOUND)));
+ }
+}
diff --git a/src/test/java/solutions/fairdata/fdp/index/acceptance/api/ping/ReceivePing_POST_Test.java b/src/test/java/solutions/fairdata/fdp/index/acceptance/api/ping/ReceivePing_POST_Test.java
index 791766c..7ad464e 100644
--- a/src/test/java/solutions/fairdata/fdp/index/acceptance/api/ping/ReceivePing_POST_Test.java
+++ b/src/test/java/solutions/fairdata/fdp/index/acceptance/api/ping/ReceivePing_POST_Test.java
@@ -66,7 +66,7 @@ private PingDTO reqDTO(String clientUrl) {
@Test
@DisplayName("HTTP 204: new entry")
- public void res204_newEnty() {
+ public void res204_newEntry() {
// GIVEN (prepare data)
String clientUrl = "http://example.com";
mongoTemplate.getDb().drop();
diff --git a/src/test/java/solutions/fairdata/fdp/index/fixtures/TokenFixtures.java b/src/test/java/solutions/fairdata/fdp/index/fixtures/TokenFixtures.java
new file mode 100644
index 0000000..3265f60
--- /dev/null
+++ b/src/test/java/solutions/fairdata/fdp/index/fixtures/TokenFixtures.java
@@ -0,0 +1,48 @@
+/**
+ * The MIT License
+ * Copyright © 2020 https://fairdata.solutions
+ *
+ * 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 solutions.fairdata.fdp.index.fixtures;
+
+import solutions.fairdata.fdp.index.entity.Token;
+
+import java.util.List;
+
+public class TokenFixtures {
+
+ public static Token adminToken() {
+ var token = new Token();
+ token.setName("admin");
+ token.setNote("This is admin token for tests");
+ token.setToken("myVerySecretToken123");
+ token.setRoles(List.of("ROLE_ADMIN"));
+ return token;
+ }
+
+ public static Token noRoleToken() {
+ var token = new Token();
+ token.setName("no_role");
+ token.setNote("This is admin token for tests");
+ token.setToken("myTokenWithoutAnyRole");
+ token.setRoles(List.of());
+ return token;
+ }
+}