Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CSRF tokens to pass-data-client #116

Merged
merged 2 commits into from
Jun 26, 2024
Merged
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
87 changes: 31 additions & 56 deletions pass-data-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<properties>
<!-- Properties for dependency versions -->
<spring-boot-maven-plugin.version>3.2.1</spring-boot-maven-plugin.version>
<okhttp.version>4.12.0</okhttp.version>
<moshi.version>4.12.0</moshi.version>
<jsonapi-adapters.version>1.1.0</jsonapi-adapters.version>
Expand All @@ -19,20 +20,17 @@
<commons-lang3.version>3.14.0</commons-lang3.version>
<junit.jupiter.version>5.10.1</junit.jupiter.version>
<mockito.version>5.10.0</mockito.version>
<!-- Properties for integration tests -->
<pass.core.port>8080</pass.core.port>
<pass.core.url>http://localhost:8080</pass.core.url>
<pass.core.user>backend</pass.core.user>
<pass.core.password>backend</pass.core.password>
<testcontainers.version>1.19.4</testcontainers.version>
<maven-model.version>3.9.6</maven-model.version>
</properties>

<dependencies>
<dependency>
<groupId>com.markomilos.jsonapi</groupId>
<artifactId>jsonapi-adapters</artifactId>
<version>${jsonapi-adapters.version}</version>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
Expand Down Expand Up @@ -80,58 +78,30 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot-maven-plugin.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<version>${maven-model.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<executions>
<execution>
<id>start</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
<configuration>
<images>
<image>
<name>ghcr.io/eclipse-pass/pass-core-main:%v</name>
<run>
<platform>${docker.platforms}</platform>
<env>
<PASS_CORE_BASE_URL>${pass.core.url}</PASS_CORE_BASE_URL>
<PASS_CORE_BACKEND_USER>${pass.core.user}</PASS_CORE_BACKEND_USER>
<PASS_CORE_BACKEND_PASSWORD>${pass.core.password}</PASS_CORE_BACKEND_PASSWORD>
</env>
<wait>
<http>
<url>
${pass.core.url}/data/grant
</url>
<status>401</status>
</http>
<time>60000</time>
</wait>
<ports>
<port>${pass.core.port}:${pass.core.port}</port>
</ports>
</run>
</image>
</images>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
Expand All @@ -147,7 +117,7 @@
<systemPropertyVariables>
<pass.core.url>${pass.core.url}</pass.core.url>
<pass.core.user>${pass.core.user}</pass.core.user>
<pass.core.password>${pass.core.password}</pass.core.password>
<pass.core.password>${pass.core.password}</pass.core.password>
</systemPropertyVariables>
</configuration>
</plugin>
Expand Down Expand Up @@ -194,12 +164,17 @@
<!-- These both come from junit-jupiter, so version is tied to that direct dependency -->
<ignoredUsedUndeclaredDependency>org.junit.jupiter:junit-jupiter-api</ignoredUsedUndeclaredDependency>
<ignoredUsedUndeclaredDependency>org.junit.jupiter:junit-jupiter-params</ignoredUsedUndeclaredDependency>
<!-- These come from testcontainers junit-jupiter -->
<ignoredUsedUndeclaredDependency>org.testcontainers::</ignoredUsedUndeclaredDependency>
<ignoredUsedUndeclaredDependency>com.github.docker-java::</ignoredUsedUndeclaredDependency>
<ignoredUsedUndeclaredDependency>org.springframework:spring-test</ignoredUsedUndeclaredDependency>
</ignoredUsedUndeclaredDependencies>
<ignoredUnusedDeclaredDependencies>
<!-- slf4j is the API used in the code, logback is the logging provider not used directly -->
<ignoredUnusedDeclaredDependency>ch.qos.logback:logback-classic</ignoredUnusedDeclaredDependency>
<!-- junit-jupiter is a module containing the junit api jars used directly -->
<ignoredUnusedDeclaredDependency>org.junit.jupiter:junit-jupiter</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>org.springframework.boot:spring-boot-starter-test</ignoredUnusedDeclaredDependency>
</ignoredUnusedDeclaredDependencies>
</configuration>
</execution>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ public JsonApiPassClient(String baseUrl, String user, String pass) {
client_builder.addInterceptor(new OkHttpBasicAuthInterceptor(user, pass));
}

client_builder.addInterceptor(new OkHttpCsrfInterceptor());

client = client_builder.build();
moshi = create_moshi(false);

Expand Down Expand Up @@ -176,14 +178,16 @@ public <T extends PassEntity> void createObject(T obj) throws IOException {
String url = baseUrl + "data/" + get_json_type(obj.getClass());
RequestBody body = RequestBody.create(json, JSON_API_MEDIA_TYPE);
Request request = new Request.Builder().url(url).header("Accept", JSON_API_CONTENT_TYPE)
.addHeader("Content-Type", JSON_API_CONTENT_TYPE).post(body).build();
.header("Content-Type", JSON_API_CONTENT_TYPE).post(body).build();

try (Response response = client.newCall(request).execute()) {
String result = response.body().string();

if (!response.isSuccessful()) {
throw new IOException(
"Create failed: " + url + " returned " + response.code() + " " + response.body().string());
"Create failed: " + url + " returned " + response.code() + " " + result);
}
Document<T> result_doc = adapter.fromJson(response.body().string());
Document<T> result_doc = adapter.fromJson(result);
obj.setId(result_doc.requireData().getId());
setVersionIfNeeded(result_doc, obj);
}
Expand All @@ -204,14 +208,16 @@ public <T extends PassEntity> void updateObject(T obj) throws IOException {
String url = get_url(obj);
RequestBody body = RequestBody.create(json, JSON_API_MEDIA_TYPE);
Request request = new Request.Builder().url(url).header("Accept", JSON_API_CONTENT_TYPE)
.addHeader("Content-Type", JSON_API_CONTENT_TYPE).patch(body).build();
.header("Content-Type", JSON_API_CONTENT_TYPE).patch(body).build();

try (Response response = client.newCall(request).execute()) {
String result = response.body().string();

if (!response.isSuccessful()) {
throw new IOException(
"Update failed: " + url + " returned " + response.code() + " " + response.body().string());
"Update failed: " + url + " returned " + response.code() + " " + result);
}
Document<T> result_doc = adapter.fromJson(response.body().string());
Document<T> result_doc = adapter.fromJson(result);
setVersionIfNeeded(result_doc, obj);
}
}
Expand Down Expand Up @@ -557,7 +563,7 @@ public <T extends PassEntity> T getObject(Class<T> type, String id, String... in
HttpUrl url = url_builder.build();

Request request = new Request.Builder().url(url).header("Accept", JSON_API_CONTENT_TYPE)
.addHeader("Content-Type", JSON_API_CONTENT_TYPE).get().build();
.header("Content-Type", JSON_API_CONTENT_TYPE).get().build();

String body;
try (Response response = client.newCall(request).execute()) {
Expand Down Expand Up @@ -586,10 +592,11 @@ public <T extends PassEntity> void deleteObject(Class<T> type, String id) throws
String url = get_url(type, id);

Request request = new Request.Builder().url(url).delete().build();

try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException(
"Delete failed: " + url + " returned " + response.code() + " " + response.body().string());
"Delete failed: " + url + " returned " + response.code());
}
}
}
Expand Down Expand Up @@ -620,7 +627,7 @@ public <T extends PassEntity> PassClientResult<T> selectObjects(PassClientSelect
HttpUrl url = url_builder.build();

Request request = new Request.Builder().url(url).header("Accept", JSON_API_CONTENT_TYPE)
.addHeader("Content-Type", JSON_API_CONTENT_TYPE).get().build();
.header("Content-Type", JSON_API_CONTENT_TYPE).get().build();

String body;
try (Response response = client.newCall(request).execute()) {
Expand Down Expand Up @@ -700,17 +707,15 @@ public URI uploadBinary(String name, byte[] data) throws IOException {
.addEncodedPathSegment("file").build();

RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart("file", name, RequestBody.create(data))
.build();
.addFormDataPart("file", name, RequestBody.create(data)).build();

Request request = new Request.Builder().url(url).post(body).build();

try (Response response = client.newCall(request).execute()) {

if (!response.isSuccessful()) {
throw new IOException(
"File upload failed: " + url + " returned " + response.code()
+ " " + response.body().string());
"File upload failed: " + url + " returned " + response.code());
}

// Grab the id field
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.eclipse.pass.support.client;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
* Add CSRF token as a header and cookie to requests.
* The token can have any value.
*/
public class OkHttpCsrfInterceptor implements Interceptor {
private static String CSRF_TOKEN = "anyvalue";

@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder().header("X-XSRF-TOKEN", CSRF_TOKEN)
.header("Cookie", "XSRF-TOKEN=" + CSRF_TOKEN).build();

return chain.proceed(request);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.eclipse.pass.support.client;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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 java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
Expand All @@ -17,9 +19,19 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.github.dockerjava.api.model.ExposedPort;
import com.github.dockerjava.api.model.PortBinding;
import com.github.dockerjava.api.model.Ports;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.eclipse.pass.support.client.model.AggregatedDepositStatus;
import org.eclipse.pass.support.client.model.AwardStatus;
import org.eclipse.pass.support.client.model.CopyStatus;
Expand All @@ -45,15 +57,49 @@
import org.eclipse.pass.support.client.model.SubmissionStatus;
import org.eclipse.pass.support.client.model.User;
import org.eclipse.pass.support.client.model.UserRole;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.test.annotation.DirtiesContext;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

@Testcontainers
@DirtiesContext
public class JsonApiPassClientIT {
private static PassClient client;
private static final DockerImageName PASS_CORE_IMG;

static {
MavenXpp3Reader reader = new MavenXpp3Reader();
try {
Model model = reader.read(new FileReader("pom.xml"));
String version = model.getParent().getVersion();
PASS_CORE_IMG = DockerImageName.parse("ghcr.io/eclipse-pass/pass-core-main:" + version);
} catch (Exception e) {
throw new RuntimeException(e);
}

System.setProperty("pass.core.url", "http://localhost:8080");
System.setProperty("pass.core.user", "backend");
System.setProperty("pass.core.password", "moo");
}

@Container
private static final GenericContainer<?> PASS_CORE_CONTAINER = new GenericContainer<>(PASS_CORE_IMG)
.withCreateContainerCmdModifier(cmd -> {
cmd.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindPort(8080),
new ExposedPort(8080)));
})
.withExposedPorts(8080)
.waitingFor(Wait.forLogMessage(".*Jetty started on port 8080.*", 1));

@BeforeAll
public static void setup() {
client = PassClient.newInstance();
private JsonApiPassClient client;

@BeforeEach
public void setup() {
client = (JsonApiPassClient) PassClient.newInstance();
}

@Test
Expand Down Expand Up @@ -763,4 +809,43 @@ public void testDeleteFile() throws IOException {
File actualFile = client.getObject(File.class, file.getId());
assertNull(actualFile);
}

@Test
public void testConcurrency() throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4);

List<Future<?>> results = new ArrayList<Future<?>>();
Random rand = new Random();

for (int i = 0; i < 200; i++) {
final int num = i;

Future<?> result = pool.submit(() -> {
User user = new User();
user.setFirstName("" + num);
user.setLastName(Thread.currentThread().getName());

assertDoesNotThrow(() -> {
client.createObject(user);

Thread.sleep(rand.nextLong(100));
user.setMiddleName("middle");
client.updateObject(user);

Thread.sleep(rand.nextLong(100));
client.deleteObject(user);
});
});

results.add(result);
}

// Throw assertion failures
for (Future<?> f: results) {
f.get();
}

pool.shutdown();
pool.awaitTermination(5, TimeUnit.MINUTES);
}
}
Loading