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; + } +}