Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
- .:/app
working_dir: /app
# Run only unit tests (no real credentials). Full suite: mvn test -q with env set.
command: mvn test -Dtest=RevokeTokenInputTest,AuthClientTest,AwsAuthProviderTest,ProjectsClientTest
command: mvn test -Dtest=RevokeTokenInputTest,AuthClientTest,AwsAuthProviderTest,ProjectsClientTest,ApiClientTest

format:
image: cimg/openjdk:21.0
Expand Down
23 changes: 15 additions & 8 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@


<!-- <maven.compiler.release>21</maven.compiler.release> -->
<okhttp-version>2.7.5</okhttp-version>
<!-- 4.x is the last line supporting the Java 8 target above -->
<okhttp-version>4.12.0</okhttp-version>
<gson-version>2.8.9</gson-version>
<gson-fire-version>1.8.3</gson-fire-version>
</properties>
Expand Down Expand Up @@ -108,17 +109,11 @@

<!-- HTTP request dependencies-->
<dependency>
<groupId>com.squareup.okhttp</groupId>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp-version}</version>
</dependency>

<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>logging-interceptor</artifactId>
<version>${okhttp-version}</version>
</dependency>

<!-- JSON parsing dependencies-->
<dependency>
<groupId>com.google.code.gson</groupId>
Expand All @@ -139,6 +134,18 @@
<optional>true</optional>
</dependency>
<!-- Testing dependencies-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>${okhttp-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp-tls</artifactId>
<version>${okhttp-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
Expand Down
15 changes: 8 additions & 7 deletions src/main/java/com/infisical/sdk/api/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.google.gson.Gson;
import com.infisical.sdk.util.InfisicalException;
import com.squareup.okhttp.*;
import com.squareup.okhttp.Request;
import okhttp3.*;
import okhttp3.Request;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
Expand All @@ -14,7 +14,7 @@ public class ApiClient {
private String accessToken;
private String baseUrl;

private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");

public ApiClient(String baseUrl, String accessToken) {
this.client = new OkHttpClient();
Expand Down Expand Up @@ -77,7 +77,7 @@ public <T, R> R post(String url, T requestBody, Class<R> responseType) throws In
// Build request
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.post(RequestBody.create(JSON, jsonBody))
.post(RequestBody.create(jsonBody, JSON))
.header("Accept", "application/json");

if (this.accessToken != null && !this.accessToken.isEmpty()) {
Expand Down Expand Up @@ -119,7 +119,8 @@ public <R> R post(String url, Class<R> responseType) throws InfisicalException {
public <R> R get(String baseUrl, Map<String, String> queryParams, Class<R> responseType)
throws InfisicalException {
try {
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
// get() throws on a malformed URL rather than returning null
HttpUrl.Builder urlBuilder = HttpUrl.get(baseUrl).newBuilder();

if (queryParams != null) {
queryParams.forEach(urlBuilder::addQueryParameter);
Expand Down Expand Up @@ -163,7 +164,7 @@ public <T, R> R patch(String url, T requestBody, Class<R> responseType)
// Build request
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.patch(RequestBody.create(JSON, jsonBody))
.patch(RequestBody.create(jsonBody, JSON))
.header("Accept", "application/json");

if (this.accessToken != null && !this.accessToken.isEmpty()) {
Expand Down Expand Up @@ -199,7 +200,7 @@ public <T, R> R delete(String url, T requestBody, Class<R> responseType)
// Build request
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.delete(RequestBody.create(JSON, jsonBody))
.delete(RequestBody.create(jsonBody, JSON))
.header("Accept", "application/json");

if (this.accessToken != null && !this.accessToken.isEmpty()) {
Expand Down
214 changes: 214 additions & 0 deletions src/test/java/com/infisical/sdk/api/ApiClientTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package com.infisical.sdk.api;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.infisical.sdk.util.InfisicalException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.net.ssl.SSLPeerUnverifiedException;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okhttp3.tls.HandshakeCertificates;
import okhttp3.tls.HeldCertificate;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class ApiClientTest {

private MockWebServer server;

public static class Payload {
private final String name;

public Payload(String name) {
this.name = name;
}
}

public static class Reply {
private String message;

public String getMessage() {
return message;
}
}

@BeforeEach
public void startServer() throws IOException {
server = new MockWebServer();
server.start();
}

@AfterEach
public void stopServer() throws IOException {
server.shutdown();
}

private ApiClient apiClient(String accessToken) {
return new ApiClient(server.url("/").toString(), accessToken);
}

@Test
public void post_sendsJsonBodyWithAuthHeaders() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"message\":\"created\"}"));

Reply reply = apiClient("token-123")
.post(server.url("/api/v3/secrets/raw").toString(), new Payload("SECRET"), Reply.class);

assertEquals("created", reply.getMessage());

RecordedRequest request = server.takeRequest();
assertEquals("POST", request.getMethod());
assertEquals("/api/v3/secrets/raw", request.getPath());
assertEquals("Bearer token-123", request.getHeader("Authorization"));
assertEquals("application/json", request.getHeader("Accept"));
assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
assertEquals("{\"name\":\"SECRET\"}", request.getBody().readUtf8());
}

@Test
public void get_appendsQueryParametersAndAuthHeader() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"message\":\"ok\"}"));

Map<String, String> queryParams = new LinkedHashMap<>();
queryParams.put("workspaceId", "project-1");
queryParams.put("environment", "dev");

apiClient("token-123").get(server.url("/api/v3/secrets/raw").toString(), queryParams, Reply.class);

RecordedRequest request = server.takeRequest();
assertEquals("GET", request.getMethod());

HttpUrl url = request.getRequestUrl();
assertEquals("/api/v3/secrets/raw", url.encodedPath());
assertEquals("project-1", url.queryParameter("workspaceId"));
assertEquals("dev", url.queryParameter("environment"));
assertEquals("Bearer token-123", request.getHeader("Authorization"));
}

@Test
public void patch_usesPatchVerbAndSendsBody() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"message\":\"updated\"}"));

Reply reply = apiClient("token-123")
.patch(server.url("/api/v3/secrets/raw/SECRET").toString(), new Payload("SECRET"), Reply.class);

assertEquals("updated", reply.getMessage());

RecordedRequest request = server.takeRequest();
assertEquals("PATCH", request.getMethod());
assertEquals("{\"name\":\"SECRET\"}", request.getBody().readUtf8());
}

@Test
public void delete_usesDeleteVerbAndSendsBody() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"message\":\"deleted\"}"));

Reply reply = apiClient("token-123")
.delete(server.url("/api/v3/secrets/raw/SECRET").toString(), new Payload("SECRET"), Reply.class);

assertEquals("deleted", reply.getMessage());

RecordedRequest request = server.takeRequest();
assertEquals("DELETE", request.getMethod());
assertEquals("{\"name\":\"SECRET\"}", request.getBody().readUtf8());
}

@Test
public void post_withoutAccessToken_omitsAuthorizationHeader() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"message\":\"ok\"}"));

apiClient(null)
.post(server.url("/api/v1/auth/universal-auth/login").toString(), new Payload("id"), Reply.class);

assertNull(server.takeRequest().getHeader("Authorization"));
}

@Test
public void post_withEmptyResponseBody_returnsNull() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200));

Void result = apiClient("token-123")
.post(server.url("/api/v1/auth/token/revoke").toString(), new Payload("token"), Void.class);

assertNull(result);
}

@Test
public void errorResponse_throwsInfisicalExceptionCarryingResponseBody() {
server.enqueue(
new MockResponse().setResponseCode(403).setBody("{\"message\":\"permission denied\"}"));

InfisicalException exception = assertThrows(
InfisicalException.class,
() -> apiClient("token-123")
.get(server.url("/api/v3/secrets/raw").toString(), Collections.emptyMap(), Reply.class));

assertTrue(exception.getMessage().contains("permission denied"), exception.getMessage());
}

@Test
public void get_withMalformedBaseUrl_throwsInsteadOfNullPointer() {
assertThrows(
IllegalArgumentException.class,
() -> apiClient("token-123").get("not-a-url", Collections.emptyMap(), Reply.class));
}

// CVE-2021-0341 guard. Builds its own client, since ApiClient cannot be given the test CA.
@Test
public void https_certificateIssuedForAnotherHost_isRejected() throws Exception {
HeldCertificate rootCertificate = new HeldCertificate.Builder().certificateAuthority(0).build();
HeldCertificate serverCertificate = new HeldCertificate.Builder()
.commonName("example.com")
.addSubjectAlternativeName("example.com")
.signedBy(rootCertificate)
.build();

HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder()
.heldCertificate(serverCertificate, rootCertificate.certificate())
.build();
server.useHttps(serverCertificates.sslSocketFactory(), false);
server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"message\":\"ok\"}"));

HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder()
.addTrustedCertificate(rootCertificate.certificate())
.build();
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager())
.build();

Request request = new Request.Builder().url(server.url("/api/v3/secrets/raw")).build();

assertThrows(SSLPeerUnverifiedException.class, () -> client.newCall(request).execute());
}

@Test
public void https_untrustedCertificate_failsBeforeTheRequestIsSent() throws Exception {
HeldCertificate serverCertificate = new HeldCertificate.Builder()
.commonName("localhost")
.addSubjectAlternativeName("localhost")
.build();
HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder()
.heldCertificate(serverCertificate)
.build();
server.useHttps(serverCertificates.sslSocketFactory(), false);
server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"message\":\"ok\"}"));

assertThrows(
InfisicalException.class,
() -> apiClient("token-123")
.get(server.url("/api/v3/secrets/raw").toString(), Collections.emptyMap(), Reply.class));

assertEquals(0, server.getRequestCount());
}
}
Loading