Skip to content
Snippets Groups Projects
Commit 07186c09 authored by Konstantinos Georgilakis's avatar Konstantinos Georgilakis
Browse files

Webhooks crud actions REST API

parent 315c16f9
No related branches found
No related tags found
1 merge request!141Webhooks crud actions REST API
Pipeline #93286 passed
Showing
with 483 additions and 1 deletion
package net.geant.nmaas.portal.api.domain;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.geant.nmaas.portal.persistent.entity.WebhookEventType;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class WebhookEventDto {
private Long id;
@NotNull
private String name;
@NotNull
private String targetUrl;
@NotNull
private WebhookEventType eventType;
private String tokenValue;
@Pattern(regexp = "^(Authorization|X-.*)?$", message = "Authorization header must be either 'Authorization' or start with 'X-'")
private String authorizationHeader;
public WebhookEventDto (Long id, String name, String targetUrl, WebhookEventType eventType){
this.id = id;
this.name = name;
this.targetUrl = targetUrl;
this.eventType = eventType;
}
}
package net.geant.nmaas.portal.api.market;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice(assignableTypes = WebhookEventController.class)
public class WebhookEventAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
package net.geant.nmaas.portal.api.market;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import net.geant.nmaas.portal.api.domain.Id;
import net.geant.nmaas.portal.api.domain.WebhookEventDto;
import net.geant.nmaas.portal.api.exception.ProcessingException;
import net.geant.nmaas.portal.persistent.entity.WebhookEvent;
import net.geant.nmaas.portal.service.WebhookEventService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/webhooks")
@Slf4j
public class WebhookEventController {
private final WebhookEventService webhookEventService;
private static final String UNABLE_TO_CHANGE_WEBHOOK_EVENT = "Unable to change WebhookEvent";
@Autowired
public WebhookEventController(WebhookEventService webhookEventService){
this.webhookEventService = webhookEventService;
}
@PostMapping
@Transactional
@PreAuthorize("hasRole('ROLE_SYSTEM_ADMIN')")
public ResponseEntity<Id> createWebhook(@RequestBody @Valid WebhookEventDto webhook) {
WebhookEvent webhookEvent = null;
try {
webhookEvent = webhookEventService.create(webhook);
} catch (Exception e) {
throw new RuntimeException(e);
}
return ResponseEntity.ok(new Id(webhookEvent.getId()));
}
@PutMapping("/{id}")
@Transactional
@PreAuthorize("hasRole('ROLE_SYSTEM_ADMIN')")
public void updateWebhook(@PathVariable Long id, @RequestBody @Valid WebhookEventDto webhook){
if (!id.equals(webhook.getId())) {
throw new ProcessingException(UNABLE_TO_CHANGE_WEBHOOK_EVENT);
}
try {
webhookEventService.update(webhook);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
@DeleteMapping("/{id}")
@Transactional
@PreAuthorize("hasRole('ROLE_SYSTEM_ADMIN')")
public void deleteWebhook(@PathVariable Long id) {
webhookEventService.remove(id);
}
@GetMapping("/{id}")
@Transactional
@PreAuthorize("hasRole('ROLE_SYSTEM_ADMIN')")
public ResponseEntity<WebhookEventDto> getWebhook(@PathVariable Long id) throws Exception {
return ResponseEntity.ok(webhookEventService.getById(id));
}
@GetMapping
@Transactional
@PreAuthorize("hasRole('ROLE_SYSTEM_ADMIN')")
public ResponseEntity<List<WebhookEventDto>> getAllWebhooks() {
return ResponseEntity.ok(webhookEventService.getAllWebhooks());
}
}
package net.geant.nmaas.portal.api.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
@Service
public class EncryptionService {
private static final int GCM_TAG_LENGTH = 128;
private static final int IV_LENGTH = 12;
@Value("${security.encryption.secret-key}")
private String secretKey;
@Value("${security.encryption.algorithm}")
private String algorithm;
public String encrypt(String plainText) throws Exception {
Cipher cipher = Cipher.getInstance(algorithm);
SecretKey key = getKey();
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
byte[] encryptedData = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
byte[] combinedData = new byte[IV_LENGTH + encryptedData.length];
System.arraycopy(iv, 0, combinedData, 0, IV_LENGTH);
System.arraycopy(encryptedData, 0, combinedData, IV_LENGTH, encryptedData.length);
return Base64.getEncoder().encodeToString(combinedData);
}
public String decrypt(String encryptedText) throws Exception {
byte[] decodedData = Base64.getDecoder().decode(encryptedText);
byte[] iv = new byte[IV_LENGTH];
System.arraycopy(decodedData, 0, iv, 0, IV_LENGTH);
byte[] encryptedData = new byte[decodedData.length - IV_LENGTH];
System.arraycopy(decodedData, IV_LENGTH, encryptedData, 0, encryptedData.length);
Cipher cipher = Cipher.getInstance(algorithm);
SecretKey key = getKey();
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
byte[] decryptedData = cipher.doFinal(encryptedData);
return new String(decryptedData, StandardCharsets.UTF_8);
}
private SecretKey getKey() {
return new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
}
}
package net.geant.nmaas.portal.persistent.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "webhook")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class WebhookEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String targetUrl;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private WebhookEventType eventType;
@Column
private String tokenValue;
@Column
private String authorizationHeader;
public WebhookEvent (Long id, String name, String targetUrl, WebhookEventType eventType){
this.id = id;
this.name = name;
this.targetUrl = targetUrl;
this.eventType = eventType;
}
}
package net.geant.nmaas.portal.persistent.entity;
public enum WebhookEventType {
DOMAIN_CREATION,
APPLICATION_DEPLOYMENT,
USER_ASSIGNMENT,
DOMAIN_GROUP_CHANGE
}
package net.geant.nmaas.portal.persistent.repositories;
import net.geant.nmaas.portal.persistent.entity.WebhookEvent;
import net.geant.nmaas.portal.persistent.entity.WebhookEventType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface WebhookEventRepository extends JpaRepository<WebhookEvent, Long> {
List<WebhookEvent> findByEventType(WebhookEventType eventType);
}
package net.geant.nmaas.portal.service;
import net.geant.nmaas.portal.api.domain.WebhookEventDto;
import net.geant.nmaas.portal.api.exception.MissingElementException;
import net.geant.nmaas.portal.api.security.EncryptionService;
import net.geant.nmaas.portal.persistent.entity.WebhookEvent;
import net.geant.nmaas.portal.persistent.entity.WebhookEventType;
import net.geant.nmaas.portal.persistent.repositories.WebhookEventRepository;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class WebhookEventService {
private final WebhookEventRepository webhookRepository;
private final EncryptionService encryptionService;
private final ModelMapper modelMapper;
private static final String WEBHOOK_EVENT_NOT_FOUND = "WebhookEvent not found.";
@Autowired
public WebhookEventService(WebhookEventRepository webhookRepository, EncryptionService encryptionService, ModelMapper modelMapper){
this.webhookRepository = webhookRepository;
this.encryptionService = encryptionService;
this.modelMapper = modelMapper;
}
public WebhookEvent create(WebhookEventDto webhookEventDto) throws Exception {
WebhookEvent webhookEvent = new WebhookEvent();
setWebhookEvent(webhookEvent, webhookEventDto);
return webhookRepository.save(webhookEvent);
}
public void update(WebhookEventDto webhookEventDto) throws Exception {
WebhookEvent webhookEvent = webhookRepository.findById(webhookEventDto.getId()).orElseThrow(() -> new MissingElementException(WEBHOOK_EVENT_NOT_FOUND));
setWebhookEvent(webhookEvent, webhookEventDto);
webhookRepository.save(webhookEvent);
}
private void setWebhookEvent(WebhookEvent webhookEvent, WebhookEventDto webhookEventDto) throws Exception {
webhookEvent.setName(webhookEventDto.getName());
webhookEvent.setTargetUrl(webhookEventDto.getTargetUrl());
webhookEvent.setEventType(webhookEventDto.getEventType());
webhookEvent.setTokenValue(webhookEventDto.getTokenValue() == null ? null : encryptionService.encrypt(webhookEventDto.getTokenValue()));
webhookEvent.setAuthorizationHeader(webhookEventDto.getAuthorizationHeader());
}
public void remove(Long id){
WebhookEvent webhookEvent = webhookRepository.findById(id).orElseThrow(() -> new MissingElementException(WEBHOOK_EVENT_NOT_FOUND));
webhookRepository.delete(webhookEvent);
}
public List<WebhookEventDto> getAllWebhooks() {
return webhookRepository.findAll().stream().map(x ->
{
try {
x.setTokenValue(x.getTokenValue() == null ? null : encryptionService.decrypt(x.getTokenValue()));
} catch (Exception e) {
throw new RuntimeException(e);
}
return modelMapper.map(x, WebhookEventDto.class);
}).collect(Collectors.toList());
}
public List<WebhookEvent> getWebhooksByEvent(WebhookEventType webhookEventType) {
return webhookRepository.findByEventType(webhookEventType);
}
public WebhookEventDto getById(Long id) throws Exception {
WebhookEvent event = webhookRepository.findById(id)
.orElseThrow(() -> new MissingElementException(String.format("WebhookEventType with id: %d cannot be found", id)));
event.setTokenValue(event.getTokenValue() == null ? null : encryptionService.decrypt(event.getTokenValue()));
return modelMapper.map(event, WebhookEventDto.class);
}
}
......@@ -150,3 +150,6 @@ portal.config.sendAppInstanceFailureEmails=${PORTAL_SEND_FAILURE_NOTIF_FLAG:fals
portal.config.showDomainRegistrationSelector=${PORTAL_DOMAIN_REGISTRATION_SELECTOR:true}
# string - list of emails with ':' as a separator, e.g., admin1@nmaas.eu;admin2@nmaas.eu
portal.config.appInstanceFailureEmailList=${ADMIN_EMAIL:admin@nmaas.eu}
security.encryption.secret-key=${SECURITY_PBKDF2_SECRET:nmaasplatformgn5}
security.encryption.algorithm=${SECURITY_PBKDF2_ALGORITHM:AES/GCM/NoPadding}
\ No newline at end of file
CREATE TABLE webhookEvent (
id bigint PRIMARY KEY generated by default as identity,
name VARCHAR(255) NOT NULL unique,
target_url VARCHAR(255) NOT NULL,
event_type VARCHAR(50) NOT NULL,
token_value VARCHAR(2056),
authorization_header VARCHAR(50)
);
\ No newline at end of file
package net.geant.nmaas.portal.service;
import net.geant.nmaas.portal.api.domain.WebhookEventDto;
import net.geant.nmaas.portal.api.security.EncryptionService;
import net.geant.nmaas.portal.persistent.entity.WebhookEvent;
import net.geant.nmaas.portal.persistent.entity.WebhookEventType;
import net.geant.nmaas.portal.persistent.repositories.WebhookEventRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.modelmapper.ModelMapper;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class WebhookEventServiceTest {
WebhookEventRepository webhookEventRepository = mock(WebhookEventRepository.class);
private final ModelMapper modelMapper = new ModelMapper();
EncryptionService encryptionService = mock(EncryptionService.class);
WebhookEventService webhookEventService = new WebhookEventService(webhookEventRepository, encryptionService, modelMapper);
private WebhookEventDto webhookEventDto;
private WebhookEvent webhookEvent;
@BeforeEach
void setUp() throws Exception {
webhookEventDto = new WebhookEventDto(1L, "webhook", "https://example.com/webhook", WebhookEventType.APPLICATION_DEPLOYMENT);
webhookEvent = new WebhookEvent(1L, "webhook", "https://example.com/webhook", WebhookEventType.APPLICATION_DEPLOYMENT);
webhookEventService.create(webhookEventDto);
}
@Test
void crudWebhookEvent() throws Exception {
webhookEventDto = new WebhookEventDto(2L, "webhook2", "https://example.com/webhook2", WebhookEventType.DOMAIN_CREATION, "xxxxyyyy", "Authorization");
webhookEvent = new WebhookEvent(2L, "webhook2", "https://example.com/webhook2", WebhookEventType.DOMAIN_CREATION, "sjxV/ytRIoHjXy+CtXMzD4T+bntbqzQX25eztXbJ9r4gIZXT", "Authorization");
when(webhookEventRepository.save(isA(WebhookEvent.class))).thenReturn(webhookEvent);
when(encryptionService.encrypt(anyString())).thenAnswer(i -> "sjxV/ytRIoHjXy+CtXMzD4T+bntbqzQX25eztXbJ9r4gIZXT");
when(encryptionService.decrypt(anyString())).thenAnswer(i -> "xxxxyyyy");
WebhookEvent created = webhookEventService.create(webhookEventDto);
assertNotNull(created);
assertEquals(webhookEventDto.getId(), created.getId());
assertEquals(webhookEventDto.getName(), created.getName());
assertEquals(webhookEventDto.getTargetUrl(), created.getTargetUrl());
assertEquals(webhookEventDto.getEventType(), created.getEventType());
assertEquals(webhookEventDto.getAuthorizationHeader(), created.getAuthorizationHeader());
assertEquals(webhookEventDto.getTokenValue(), encryptionService.decrypt(created.getTokenValue()));
webhookEventDto.setName("updated webhook");
when(webhookEventRepository.findById(2L)).thenReturn(Optional.of(webhookEvent));
webhookEventService.update(webhookEventDto);
when(webhookEventRepository.existsById(2L)).thenReturn(true);
doNothing().when(webhookEventRepository).deleteById(2L);
webhookEventService.remove(2L);
}
@Test
void shouldGetAllWebhookEvents() throws Exception {
// when(encryptionService.decrypt(anyString())).thenAnswer(i -> "xxxxyyyy");
when(webhookEventRepository.findAll()).thenReturn(Arrays.asList(webhookEvent));
List<WebhookEventDto> webhooks = webhookEventService.getAllWebhooks();
assertNotNull(webhooks);
assertEquals(1, webhooks.size());
WebhookEventDto created = webhooks.get(0);
assertEquals(webhookEventDto.getId(), created.getId());
assertEquals(webhookEventDto.getName(), created.getName());
assertEquals(webhookEventDto.getTargetUrl(), created.getTargetUrl());
assertEquals(webhookEventDto.getEventType(), created.getEventType());
}
@Test
void shouldThrowExceptionWhenWebhookNotFound() throws Exception {
// when(encryptionService.decrypt(anyString())).thenAnswer(i -> "xxxxyyyy");
when(webhookEventRepository.findById(999L)).thenReturn(Optional.empty());
assertThrows(RuntimeException.class, () -> {
webhookEventService.getById(999L);
});
}
@Test
void shouldThrowExceptionWhenDeletingNonExistentWebhook() {
when(webhookEventRepository.existsById(999L)).thenReturn(false);
assertThrows(RuntimeException.class, () -> {
webhookEventService.remove(999L);
});
}
}
\ No newline at end of file
......@@ -169,3 +169,6 @@ portal.config.showDomainRegistrationSelector=true
# string - list of emails with ':' as a separator, e.g., admin1@nmaas.eu;admin2@nmaas.eu
portal.config.appInstanceFailureEmailList=admin@nmaas.eu
nmaas.platform.multi-instance=false
security.encryption.secret-key=nmaasplatformgn5
security.encryption.algorithm=AES/GCM/NoPadding
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment