dev #3
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,3 +35,6 @@ out/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Env ###
|
||||
.env*
|
||||
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM gradle:8.10.2-jdk21 AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY build.gradle.kts settings.gradle.kts gradlew ./
|
||||
COPY gradle ./gradle
|
||||
|
||||
RUN ./gradlew dependencies --no-daemon || true
|
||||
|
||||
COPY . .
|
||||
RUN ./gradlew clean bootJar --no-daemon
|
||||
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /app/build/libs/*.jar app.jar
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||||
|
||||
@@ -25,55 +25,30 @@ repositories {
|
||||
}
|
||||
|
||||
extra["springCloudVersion"] = "2025.1.0"
|
||||
extra["springModulithVersion"] = "2.0.1"
|
||||
|
||||
extra["infisicalVersion"] = "3.0.5"
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-amqp")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-liquibase")
|
||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
||||
implementation("org.springframework.boot:spring-boot-starter-quartz")
|
||||
implementation("org.springframework.boot:spring-boot-starter-restclient")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
|
||||
implementation("org.springframework.modulith:spring-modulith-events-api")
|
||||
implementation("org.springframework.modulith:spring-modulith-starter-core")
|
||||
implementation("org.springframework.modulith:spring-modulith-starter-jpa")
|
||||
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
|
||||
compileOnly("org.projectlombok:lombok")
|
||||
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
runtimeOnly("org.springframework.modulith:spring-modulith-actuator")
|
||||
runtimeOnly("org.springframework.modulith:spring-modulith-events-amqp")
|
||||
runtimeOnly("org.springframework.modulith:spring-modulith-observability")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-amqp-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-liquibase-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-mail-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-quartz-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-restclient-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-thymeleaf-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-validation-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-testcontainers")
|
||||
testImplementation("org.springframework.modulith:spring-modulith-starter-test")
|
||||
testImplementation("org.testcontainers:testcontainers-junit-jupiter")
|
||||
testImplementation("org.testcontainers:testcontainers-postgresql")
|
||||
testImplementation("org.testcontainers:testcontainers-rabbitmq")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
|
||||
// https://mvnrepository.com/artifact/com.infisical/sdk
|
||||
implementation("com.infisical:sdk:${property("infisicalVersion")}")
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom("org.springframework.modulith:spring-modulith-bom:${property("springModulithVersion")}")
|
||||
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
|
||||
}
|
||||
}
|
||||
|
||||
24
compose.yaml
24
compose.yaml
@@ -1,16 +1,16 @@
|
||||
services:
|
||||
postgres:
|
||||
image: 'postgres:latest'
|
||||
image: "postgres:latest"
|
||||
environment:
|
||||
- 'POSTGRES_DB=mydatabase'
|
||||
- 'POSTGRES_PASSWORD=secret'
|
||||
- 'POSTGRES_USER=myuser'
|
||||
- "POSTGRES_DB=mydatabase"
|
||||
- "POSTGRES_PASSWORD=secret"
|
||||
- "POSTGRES_USER=myuser"
|
||||
ports:
|
||||
- '5432'
|
||||
rabbitmq:
|
||||
image: 'rabbitmq:latest'
|
||||
environment:
|
||||
- 'RABBITMQ_DEFAULT_PASS=secret'
|
||||
- 'RABBITMQ_DEFAULT_USER=myuser'
|
||||
ports:
|
||||
- '5672'
|
||||
- "5432"
|
||||
# rabbitmq:
|
||||
# image: 'rabbitmq:latest'
|
||||
# environment:
|
||||
# - 'RABBITMQ_DEFAULT_PASS=secret'
|
||||
# - 'RABBITMQ_DEFAULT_USER=myuser'
|
||||
# ports:
|
||||
# - '5672'
|
||||
|
||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
infisical-bridge:
|
||||
build: .
|
||||
restart: always
|
||||
environment:
|
||||
INFISICAL_API_URL: ${INFISICAL_API_URL}
|
||||
INFISICAL_CLIENT_ID: ${INFISICAL_CLIENT_ID}
|
||||
INFISICAL_CLIENT_SECRET: ${INFISICAL_CLIENT_SECRET}
|
||||
INFISICAL_WEBHOOK_SECRET: ${INFISICAL_WEBHOOK_SECRET}
|
||||
DOKPLOY_API_URL: ${DOKPLOY_API_URL}
|
||||
DOKPLOY_API_KEY: ${DOKPLOY_API_KEY}
|
||||
@@ -2,12 +2,14 @@ package com.abnov.infisicalbridge;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableFeignClients
|
||||
public class InfisicalBridgeApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(InfisicalBridgeApplication.class, args);
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(InfisicalBridgeApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.abnov.infisicalbridge.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.abnov.infisicalbridge.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.disable())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/webhook/**").permitAll()
|
||||
.anyRequest().authenticated());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.abnov.infisicalbridge.dokploy;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
import com.abnov.infisicalbridge.dto.DokployComposeUpdateRequest;
|
||||
|
||||
@FeignClient(name = "dokployClient", url = "${dokploy.api-url}", configuration = DokployFeignConfig.class)
|
||||
public interface DokployClient {
|
||||
|
||||
@PostMapping("/compose.update")
|
||||
void updateCompose(@RequestBody DokployComposeUpdateRequest request);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.abnov.infisicalbridge.dokploy;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DokployFeignConfig {
|
||||
|
||||
private final DokployProperties properties;
|
||||
|
||||
@Bean
|
||||
public RequestInterceptor dokployRequestInterceptor() {
|
||||
return requestTemplate -> {
|
||||
// Add API key to every request
|
||||
requestTemplate.header("x-api-key", properties.getApiKey());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.abnov.infisicalbridge.dokploy;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "dokploy")
|
||||
@Validated
|
||||
public class DokployProperties {
|
||||
|
||||
@NotBlank(message = "Dokploy API URL is required")
|
||||
private String apiUrl;
|
||||
|
||||
@NotBlank(message = "Dokploy API KEY is required")
|
||||
private String apiKey;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.abnov.infisicalbridge.dto;
|
||||
|
||||
public record DokployComposeUpdateRequest(
|
||||
String composeId,
|
||||
String env) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.abnov.infisicalbridge.dto;
|
||||
|
||||
public record InfisicalWebhookEventResponse(
|
||||
String event,
|
||||
ProjectResponse project,
|
||||
long timestamp) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.abnov.infisicalbridge.dto;
|
||||
|
||||
public record ProjectResponse(
|
||||
String workspaceId,
|
||||
String projectId,
|
||||
String projectName,
|
||||
String environment,
|
||||
String secretPath) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.abnov.infisicalbridge.infisical;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "infisical")
|
||||
@Validated
|
||||
public class InfisicalProperties {
|
||||
|
||||
@NotBlank(message = "Infisical API URL is required")
|
||||
private String apiUrl;
|
||||
|
||||
@NotBlank(message = "Infisical client ID is required")
|
||||
private String clientId;
|
||||
|
||||
@NotBlank(message = "Infisical client secret is required")
|
||||
private String clientSecret;
|
||||
|
||||
@NotBlank(message = "Infisical webhook secret is required")
|
||||
private String webhookSecret;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.abnov.infisicalbridge.infisical;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.infisical.sdk.InfisicalSdk;
|
||||
import com.infisical.sdk.config.SdkConfig;
|
||||
import com.infisical.sdk.util.InfisicalException;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class InfisicalService {
|
||||
private final InfisicalProperties properties;
|
||||
|
||||
@Getter
|
||||
private final InfisicalSdk sdk;
|
||||
|
||||
public InfisicalService(InfisicalProperties properties) {
|
||||
this.properties = properties;
|
||||
this.sdk = initializeClient();
|
||||
}
|
||||
|
||||
private InfisicalSdk initializeClient() {
|
||||
try {
|
||||
log.info("Initializing Infisical SDK");
|
||||
var sdkInstance = new InfisicalSdk(
|
||||
new SdkConfig.Builder()
|
||||
.withSiteUrl(properties.getApiUrl())
|
||||
.build());
|
||||
|
||||
sdkInstance.Auth().UniversalAuthLogin(
|
||||
properties.getClientId(),
|
||||
properties.getClientSecret());
|
||||
|
||||
log.info("Successfully authenticated with Infisical");
|
||||
return sdkInstance;
|
||||
|
||||
} catch (InfisicalException e) {
|
||||
log.error("Failed to initialize Infisical SDK", e);
|
||||
throw new IllegalStateException("Failed to initialize Infisical SDK", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.abnov.infisicalbridge.infisical;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Service to verify Infisical webhook signatures
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class InfisicalSignatureVerifier {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int DEFAULT_TOLERANCE_SECONDS = 300; // 5 minutes
|
||||
|
||||
/**
|
||||
* Verifies the signature of an Infisical webhook request
|
||||
*
|
||||
* @param payload The raw request body as string
|
||||
* @param signatureHeader The value of x-infisical-signature header
|
||||
* @param secretKey Your secret key from Infisical
|
||||
* @return true if signature is valid, false otherwise
|
||||
*/
|
||||
public boolean verifySignature(String payload, String signatureHeader, String secretKey) {
|
||||
return verifySignature(payload, signatureHeader, secretKey, DEFAULT_TOLERANCE_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the signature with custom tolerance
|
||||
*
|
||||
* @param payload The raw request body as string
|
||||
* @param signatureHeader The value of x-infisical-signature header (format:
|
||||
* t=<timestamp>;<signature>)
|
||||
* @param secretKey Your secret key from Infisical
|
||||
* @param toleranceInSeconds Time tolerance for replay attacks
|
||||
* @return true if signature is valid, false otherwise
|
||||
*/
|
||||
public boolean verifySignature(String payload, String signatureHeader, String secretKey, int toleranceInSeconds) {
|
||||
try {
|
||||
// Parse the signature header: t=<timestamp>;<signature>
|
||||
String[] parts = signatureHeader.split(";");
|
||||
if (parts.length != 2) {
|
||||
log.error("Invalid signature header format");
|
||||
return false;
|
||||
}
|
||||
|
||||
String timestamp = parts[0].substring(2); // Remove "t="
|
||||
String receivedSignature = parts[1];
|
||||
|
||||
// Check timestamp to prevent replay attacks
|
||||
long webhookTime = Long.parseLong(timestamp);
|
||||
long currentTime;
|
||||
|
||||
// Detect if timestamp is in milliseconds or seconds
|
||||
if (timestamp.length() > 10) {
|
||||
// Timestamp is in milliseconds
|
||||
currentTime = Instant.now().toEpochMilli();
|
||||
long toleranceInMillis = (long) toleranceInSeconds * 1000;
|
||||
if (currentTime - webhookTime > toleranceInMillis) {
|
||||
log.error("Webhook timestamp is too old");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Timestamp is in seconds
|
||||
currentTime = Instant.now().getEpochSecond();
|
||||
if (currentTime - webhookTime > toleranceInSeconds) {
|
||||
log.error("Webhook timestamp is too old");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try different signature formats that Infisical might use
|
||||
|
||||
// Format 1: timestamp.payload (most common)
|
||||
String signedPayload1 = timestamp + "." + payload;
|
||||
String expectedSignature1 = generateHmacSHA256(signedPayload1, secretKey);
|
||||
|
||||
if (MessageDigest.isEqual(
|
||||
receivedSignature.getBytes(StandardCharsets.UTF_8),
|
||||
expectedSignature1.getBytes(StandardCharsets.UTF_8))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Format 2: payload only
|
||||
String expectedSignature2 = generateHmacSHA256(payload, secretKey);
|
||||
|
||||
if (MessageDigest.isEqual(
|
||||
receivedSignature.getBytes(StandardCharsets.UTF_8),
|
||||
expectedSignature2.getBytes(StandardCharsets.UTF_8))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Format 3: t=timestamp.payload (with t= prefix)
|
||||
String signedPayload3 = signatureHeader.split(";")[0] + "." + payload;
|
||||
String expectedSignature3 = generateHmacSHA256(signedPayload3, secretKey);
|
||||
|
||||
if (MessageDigest.isEqual(
|
||||
receivedSignature.getBytes(StandardCharsets.UTF_8),
|
||||
expectedSignature3.getBytes(StandardCharsets.UTF_8))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No format matched
|
||||
log.error("Signature verification failed - no matching format found");
|
||||
return false;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error verifying signature: {}" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HMAC SHA256 signature
|
||||
*/
|
||||
private String generateHmacSHA256(String data, String key)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(
|
||||
key.getBytes(StandardCharsets.UTF_8),
|
||||
HMAC_SHA256);
|
||||
mac.init(secretKeySpec);
|
||||
|
||||
byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hmacBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts byte array to hex string
|
||||
*/
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.abnov.infisicalbridge.infisical;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.abnov.infisicalbridge.dokploy.DokployClient;
|
||||
import com.abnov.infisicalbridge.dto.DokployComposeUpdateRequest;
|
||||
import com.abnov.infisicalbridge.dto.InfisicalWebhookEventResponse;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.infisical.sdk.InfisicalSdk;
|
||||
import com.infisical.sdk.models.Secret;
|
||||
import com.infisical.sdk.util.InfisicalException;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/webhook")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class InfisicalWebhookController {
|
||||
private final InfisicalSignatureVerifier signatureVerifier;
|
||||
private final InfisicalService service;
|
||||
private final InfisicalProperties infisicalProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final DokployClient dokployClient;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> handleWebhook(
|
||||
@RequestBody String payload,
|
||||
@RequestParam String dokployComposeId,
|
||||
@RequestHeader(value = "X-Infisical-Signature", required = false) String signature)
|
||||
throws InfisicalException {
|
||||
|
||||
if (signature == null || signature.isEmpty()) {
|
||||
log.warn("Missing X-Infisical-Signature header");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
if (!signatureVerifier.verifySignature(payload, signature, infisicalProperties.getWebhookSecret())) {
|
||||
log.warn("Invalid webhook signature");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
InfisicalWebhookEventResponse webhookEvent;
|
||||
try {
|
||||
webhookEvent = objectMapper.readValue(payload, InfisicalWebhookEventResponse.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Invalid webhook payload", e);
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
InfisicalSdk sdk = service.getSdk();
|
||||
List<Secret> secrets = sdk.Secrets().ListSecrets(
|
||||
webhookEvent.project().projectId(),
|
||||
webhookEvent.project().environment(),
|
||||
webhookEvent.project().secretPath(),
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
|
||||
if (secrets.isEmpty()) {
|
||||
log.warn("No secrets found for project={} env={}",
|
||||
webhookEvent.project().projectName(),
|
||||
webhookEvent.project().environment());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
String envContent = secrets.stream()
|
||||
.map(s -> s.getSecretKey() + "=" + s.getSecretValue())
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
try {
|
||||
dokployClient.updateCompose(
|
||||
new DokployComposeUpdateRequest(dokployComposeId, envContent));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to update Dokploy compose {}", dokployComposeId, e);
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,13 @@
|
||||
spring:
|
||||
application:
|
||||
name: infisical-bridge
|
||||
|
||||
infisical:
|
||||
api-url: ${INFISICAL_API_URL}
|
||||
client-id: ${INFISICAL_CLIENT_ID}
|
||||
client-secret: ${INFISICAL_CLIENT_SECRET}
|
||||
webhook-secret: ${INFISICAL_WEBHOOK_SECRET}
|
||||
|
||||
dokploy:
|
||||
api-url: ${DOKPLOY_API_URL}
|
||||
api-key: ${DOKPLOY_API_KEY}
|
||||
|
||||
Reference in New Issue
Block a user