diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/TestkitState.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/TestkitState.java index 3822823c56..d5ab468f63 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/TestkitState.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/TestkitState.java @@ -43,6 +43,7 @@ import neo4j.org.testkit.backend.messages.responses.TestkitResponse; import org.neo4j.driver.AuthTokenManager; import org.neo4j.driver.BookmarkManager; +import org.neo4j.driver.ClientCertificateManager; import org.neo4j.driver.Logging; import org.neo4j.driver.internal.cluster.RoutingTableRegistry; import reactor.core.publisher.Mono; @@ -54,6 +55,8 @@ public class TestkitState { private static final String RESULT_NOT_FOUND_MESSAGE = "Could not find result"; private static final String BOOKMARK_MANAGER_NOT_FOUND_MESSAGE = "Could not find bookmark manager"; private static final String AUTH_PROVIDER_NOT_FOUND_MESSAGE = "Could not find authentication provider"; + private static final String CLIENT_CERTIFICATE_PROVIDER_NOT_FOUND_MESSAGE = + "Could not find client certificate provider"; private final Map driverIdToDriverHolder = new HashMap<>(); @@ -79,6 +82,7 @@ public class TestkitState { private final Map bookmarkManagerIdToBookmarkManager = new HashMap<>(); private final Logging logging; private final Map authProviderIdToAuthProvider = new HashMap<>(); + private final Map managerIdToClientCertificateManager = new HashMap<>(); @Getter private final Map errors = new HashMap<>(); @@ -260,6 +264,20 @@ public void removeAuthProvider(String id) { } } + public void addClientCertificateManager(String id, ClientCertificateManager manager) { + managerIdToClientCertificateManager.put(id, manager); + } + + public ClientCertificateManager getClientCertificateManager(String id) { + return get(id, managerIdToClientCertificateManager, CLIENT_CERTIFICATE_PROVIDER_NOT_FOUND_MESSAGE); + } + + public void removeClientCertificateManager(String id) { + if (managerIdToClientCertificateManager.remove(id) == null) { + throw new RuntimeException(CLIENT_CERTIFICATE_PROVIDER_NOT_FOUND_MESSAGE); + } + } + private String add(T value, Map idToT) { var id = newId(); idToT.put(id, value); diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderClose.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderClose.java new file mode 100644 index 0000000000..4526a3a705 --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderClose.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package neo4j.org.testkit.backend.messages.requests; + +import lombok.Getter; +import lombok.Setter; +import neo4j.org.testkit.backend.TestkitState; +import neo4j.org.testkit.backend.messages.responses.ClientCertificateProvider; +import neo4j.org.testkit.backend.messages.responses.TestkitResponse; + +@Setter +@Getter +public class ClientCertificateProviderClose extends AbstractBasicTestkitRequest { + private AuthTokenManagerCloseBody data; + + @Override + protected TestkitResponse processAndCreateResponse(TestkitState testkitState) { + testkitState.removeClientCertificateManager(data.getId()); + return ClientCertificateProvider.builder() + .data(ClientCertificateProvider.ClientCertificateProviderBody.builder() + .id(data.getId()) + .build()) + .build(); + } + + @Setter + @Getter + public static class AuthTokenManagerCloseBody { + private String id; + } +} diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderCompleted.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderCompleted.java new file mode 100644 index 0000000000..638901d684 --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ClientCertificateProviderCompleted.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package neo4j.org.testkit.backend.messages.requests; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class ClientCertificateProviderCompleted implements TestkitCallbackResult { + private ClientCertificateProviderCompletedBody data; + + @Override + public String getCallbackId() { + return data.getRequestId(); + } + + @Setter + @Getter + public static class ClientCertificateProviderCompletedBody { + private String requestId; + private ClientCertificate clientCertificate; + private boolean hasUpdate; + } +} diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewClientCertificateProvider.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewClientCertificateProvider.java new file mode 100644 index 0000000000..ad9094fc3d --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewClientCertificateProvider.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package neo4j.org.testkit.backend.messages.requests; + +import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import lombok.Getter; +import lombok.Setter; +import neo4j.org.testkit.backend.TestkitState; +import neo4j.org.testkit.backend.messages.responses.ClientCertificateProvider; +import neo4j.org.testkit.backend.messages.responses.ClientCertificateProviderRequest; +import neo4j.org.testkit.backend.messages.responses.TestkitCallback; +import neo4j.org.testkit.backend.messages.responses.TestkitResponse; +import org.neo4j.driver.ClientCertificate; +import org.neo4j.driver.ClientCertificateManager; +import org.neo4j.driver.ClientCertificates; + +@Setter +@Getter +public class NewClientCertificateProvider extends AbstractBasicTestkitRequest { + private NewClientCertificateProviderBody data; + + @Override + protected TestkitResponse processAndCreateResponse(TestkitState testkitState) { + var id = testkitState.newId(); + testkitState.addClientCertificateManager(id, new TestkitClientCertificateManager(id, testkitState)); + return ClientCertificateProvider.builder() + .data(ClientCertificateProvider.ClientCertificateProviderBody.builder() + .id(id) + .build()) + .build(); + } + + private record TestkitClientCertificateManager(String id, TestkitState testkitState) + implements ClientCertificateManager { + @Override + public CompletionStage getCertificate() { + var callbackId = testkitState.newId(); + + var callback = ClientCertificateProviderRequest.builder() + .data(ClientCertificateProviderRequest.ClientCertificateProviderRequestBody.builder() + .clientCertificateProviderId(id) + .build()) + .build(); + + ClientCertificate clientCertificate = null; + var callbackStage = dispatchTestkitCallback(testkitState, callback); + try { + var response = callbackStage.toCompletableFuture().get(); + if (response instanceof ClientCertificateProviderCompleted clientCertificateComplete) { + var data = clientCertificateComplete.getData(); + var certificateData = data.getClientCertificate().getData(); + var hasUpdate = data.isHasUpdate(); + clientCertificate = ClientCertificates.of( + Paths.get(certificateData.getCertfile()).toFile(), + Paths.get(certificateData.getKeyfile()).toFile(), + certificateData.getPassword(), + hasUpdate); + } + } catch (Exception e) { + throw new RuntimeException("Unexpected failure during Testkit callback", e); + } + return CompletableFuture.completedStage(clientCertificate); + } + + private CompletionStage dispatchTestkitCallback( + TestkitState testkitState, TestkitCallback response) { + var future = new CompletableFuture(); + testkitState.getCallbackIdToFuture().put(response.getCallbackId(), future); + testkitState.getResponseWriter().accept(response); + return future; + } + } + + @Setter + @Getter + public static class NewClientCertificateProviderBody {} +} diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java index 318b495df4..d09b4c38f6 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewDriver.java @@ -104,14 +104,16 @@ public TestkitResponse process(TestkitState testkitState) { configBuilder.withNotificationConfig( toNotificationConfig(data.notificationsMinSeverity, data.notificationsDisabledCategories)); configBuilder.withDriverMetrics(); - var clientCertificateManager = Optional.ofNullable(data.getClientCertificate()) - .map(ClientCertificate::getData) - .map(certificateData -> ClientCertificates.of( - Paths.get(certificateData.getCertfile()).toFile(), - Paths.get(certificateData.getKeyfile()).toFile(), - certificateData.getPassword(), - true)) - .map(ClientCertificateManagers::rotating) + var clientCertificateManager = Optional.ofNullable(data.getClientCertificateProviderId()) + .map(testkitState::getClientCertificateManager) + .or(() -> Optional.ofNullable(data.getClientCertificate()) + .map(ClientCertificate::getData) + .map(certificateData -> ClientCertificates.of( + Paths.get(certificateData.getCertfile()).toFile(), + Paths.get(certificateData.getKeyfile()).toFile(), + certificateData.getPassword(), + true)) + .map(ClientCertificateManagers::rotating)) .orElse(null); configBuilder.withLogging(testkitState.getLogging()); org.neo4j.driver.Driver driver; @@ -309,6 +311,7 @@ public static class NewDriverBody { private List trustedCertificates; private Boolean telemetryDisabled; private ClientCertificate clientCertificate; + private String clientCertificateProviderId; } @RequiredArgsConstructor diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java index 9a7ea00bc3..237840feb7 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java @@ -73,7 +73,10 @@ @JsonSubTypes.Type(FakeTimeTick.class), @JsonSubTypes.Type(FakeTimeUninstall.class), @JsonSubTypes.Type(CheckSessionAuthSupport.class), - @JsonSubTypes.Type(VerifyAuthentication.class) + @JsonSubTypes.Type(VerifyAuthentication.class), + @JsonSubTypes.Type(NewClientCertificateProvider.class), + @JsonSubTypes.Type(ClientCertificateProviderCompleted.class), + @JsonSubTypes.Type(ClientCertificateProviderClose.class) }) public interface TestkitRequest { TestkitResponse process(TestkitState testkitState); diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProvider.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProvider.java new file mode 100644 index 0000000000..fca91060bb --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package neo4j.org.testkit.backend.messages.responses; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ClientCertificateProvider implements TestkitResponse { + private ClientCertificateProviderBody data; + + @Override + public String testkitName() { + return "ClientCertificateProvider"; + } + + @Getter + @Builder + public static class ClientCertificateProviderBody { + private String id; + } +} diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProviderRequest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProviderRequest.java new file mode 100644 index 0000000000..d00d79d6db --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ClientCertificateProviderRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package neo4j.org.testkit.backend.messages.responses; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ClientCertificateProviderRequest implements TestkitCallback { + private ClientCertificateProviderRequestBody data; + + @Override + public String getCallbackId() { + return data.getId(); + } + + @Override + public String testkitName() { + return "ClientCertificateProviderRequest"; + } + + @Getter + @Builder + public static class ClientCertificateProviderRequestBody { + private String id; + private String clientCertificateProviderId; + } +}