8 Commits

Author SHA1 Message Date
dbb21f85bf prepare release of v1.0.1
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 11:42:16 +01:00
61dcfc79d3 update test and build dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 11:37:32 +01:00
63e7af552f make system-lambda dependency test-only (#58)
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 11:36:35 +01:00
5e2d37797e rename test classes
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 11:20:04 +01:00
b4a822bf10 use failsafe plugin for integration tests instead of "offline" profile 2021-11-21 11:19:08 +01:00
4045b1a4fd test against Vault 1.9.0
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-19 20:19:33 +01:00
6a73bc39d3 connector: remove unused DEFAULT_TLS_VERSION field
All checks were successful
continuous-integration/drone/push Build is passing
This constant was left over from the RequestHelper refactoring. It is
not used anywhere in our code and likely not used by any downstream
project, so we remove it now.
2021-10-26 20:05:37 +02:00
75561a0540 passthrough null as port number in builder (#56)
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-12 20:58:59 +02:00
11 changed files with 1649 additions and 1634 deletions

View File

@ -16,7 +16,7 @@ steps:
- name: unit-tests
image: maven:3-jdk-11
commands:
- mvn -B resources:testResources compiler:testCompile surefire:test -P offline-tests
- mvn -B test
when:
branch:
- develop
@ -25,15 +25,15 @@ steps:
- name: unit-integration-tests
image: maven:3-jdk-11
environment:
VAULT_VERSION: 1.8.3
VAULT_VERSION: 1.9.0
commands:
- curl -s -o vault_1.8.3_linux_amd64.zip https://releases.hashicorp.com/vault/1.8.3/vault_1.8.3_linux_amd64.zip
- curl -s https://releases.hashicorp.com/vault/1.8.3/vault_1.8.3_SHA256SUMS | grep linux_amd64 | sha256sum -c
- unzip vault_1.8.3_linux_amd64.zip
- rm vault_1.8.3_linux_amd64.zip
- curl -s -o vault_1.9.0_linux_amd64.zip https://releases.hashicorp.com/vault/1.9.0/vault_1.9.0_linux_amd64.zip
- curl -s https://releases.hashicorp.com/vault/1.9.0/vault_1.9.0_SHA256SUMS | grep linux_amd64 | sha256sum -c
- unzip vault_1.9.0_linux_amd64.zip
- rm vault_1.9.0_linux_amd64.zip
- mv vault /bin/
- mvn -B resources:testResources compiler:testCompile surefire:test
- mvn -B -P integration-test verify
when:
branch:
- main
- release/*
- release/*

View File

@ -6,10 +6,10 @@ jobs:
strategy:
matrix:
jdk: [ 11, 17 ]
vault: [ '1.8.3' ]
vault: [ '1.9.0' ]
include:
- jdk: 11
vault: '1.8.3'
vault: '1.9.0'
analysis: true
steps:
- name: Checkout
@ -21,6 +21,8 @@ jobs:
with:
java-version: ${{ matrix.jdk }}
distribution: 'temurin'
- name: Compile
run: mvn -B clean compile
- name: Set up Vault
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')
run: |
@ -29,14 +31,14 @@ jobs:
unzip "vault_${{ matrix.vault }}_linux_amd64.zip"
rm "vault_${{ matrix.vault }}_linux_amd64.zip"
sudo mv vault /usr/bin/vault
- name: Test
- name: Test (Unit & Integration)
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')
env:
VAULT_VERSION: ${{ matrix.vault }}
run: mvn -B -P coverage clean verify
- name: Test (offline)
run: mvn -B -P coverage -P integration-test verify
- name: Test (Unit)
if: github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/')
run: mvn -B -P offline-tests -P coverage clean verify
run: mvn -B -P coverage verify
- name: Analysis
if: matrix.analysis && github.event_name == 'push'
run: >

View File

@ -1,3 +1,12 @@
## 1.0.1 (2021-11-21)
### Fix
* Make `HTTPVaultConnectorBuilder#withPort(Integer)` null-safe (#56)
* Make system-lambda dependency test-only (#58)
### Test
* Tested against Vault 1.9.0
## 1.0.0 (2021-10-02)
### Breaking

View File

@ -32,7 +32,7 @@ Java Vault Connector is a connector library for [Vault](https://www.vaultproject
* SQL secret handling
* KV v1 and v2 support
* Connector Factory with builder pattern
* Tested against Vault 1.8.3
* Tested against Vault 1.9.0
## Maven Artifact
@ -40,7 +40,7 @@ Java Vault Connector is a connector library for [Vault](https://www.vaultproject
<dependency>
<groupId>de.stklcode.jvault</groupId>
<artifactId>jvault-connector</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
</dependency>
```

37
pom.xml
View File

@ -4,7 +4,7 @@
<groupId>de.stklcode.jvault</groupId>
<artifactId>jvault-connector</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
<packaging>jar</packaging>
@ -128,13 +128,14 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<version>4.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-lambda</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
@ -155,7 +156,7 @@
<dependency>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.0.2155</version>
<version>3.9.1.2184</version>
</dependency>
</dependencies>
</dependencyManagement>
@ -266,19 +267,23 @@
</profile>
<profile>
<id>offline-tests</id>
<id>integration-test</id>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>online</excludedGroups>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
@ -289,7 +294,7 @@
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>6.3.2</version>
<version>6.5.0</version>
<executions>
<execution>
<goals>

View File

@ -63,8 +63,6 @@ public class HTTPVaultConnector implements VaultConnector {
private static final String PATH_UNDELETE = "/undelete/";
private static final String PATH_DESTROY = "/destroy/";
public static final String DEFAULT_TLS_VERSION = "TLSv1.2";
private final RequestHelper request;
private boolean authorized = false; // Authorization status.

View File

@ -125,13 +125,13 @@ public final class HTTPVaultConnectorBuilder {
/**
* Set port (default: 8200).
* A value of {@code null} or {@code -1} indicates that no port is specified, i.e. the protocol default is used.
* Otherwise a valid port number bwetween 1 and 65535 is expected.
* Otherwise, a valid port number between 1 and 65535 is expected.
*
* @param port Vault TCP port
* @return self
*/
public HTTPVaultConnectorBuilder withPort(final Integer port) {
if (port < 0) {
if (port == null || port < 0) {
this.port = null;
} else if (port < 1 || port > 65535) {
throw new IllegalArgumentException("Port number " + port + " out of range");

View File

@ -89,6 +89,8 @@ class HTTPVaultConnectorBuilderTest {
assertThrows(IllegalArgumentException.class, () -> HTTPVaultConnector.builder().withPort(0), "Port number 0 should throw an exception");
builder = assertDoesNotThrow(() -> HTTPVaultConnector.builder().withPort(-1), "Port number -1 should not throw an exception");
assertNull(builder.getPort(), "Port number -1 should be omitted");
builder = assertDoesNotThrow(() -> HTTPVaultConnector.builder().withPort(null), "Port number NULL should not throw an exception");
assertNull(builder.getPort(), "Port number NULL should be passed through");
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -1,343 +0,0 @@
/*
* Copyright 2016-2021 Stefan Kalscheuer
*
* 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 de.stklcode.jvault.connector;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import de.stklcode.jvault.connector.exception.ConnectionException;
import de.stklcode.jvault.connector.exception.InvalidResponseException;
import de.stklcode.jvault.connector.exception.PermissionDeniedException;
import de.stklcode.jvault.connector.exception.VaultConnectorException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.ServerSocket;
import java.net.URISyntaxException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
import static org.junit.jupiter.api.Assertions.*;
/**
* JUnit test for HTTP Vault connector.
* This test suite contains tests that do not require connection to an actual Vault instance.
*
* @author Stefan Kalscheuer
* @since 0.7.0
*/
class HTTPVaultConnectorOfflineTest {
private static WireMockServer wireMock;
@BeforeAll
static void prepare() {
// Initialize HTTP mock.
wireMock = new WireMockServer(WireMockConfiguration.options().dynamicPort());
wireMock.start();
WireMock.configureFor("localhost", wireMock.port());
}
@AfterAll
static void tearDown() {
wireMock.stop();
wireMock = null;
}
/**
* Test exceptions thrown during request.
*/
@Test
void requestExceptionTest() throws IOException, URISyntaxException {
HTTPVaultConnector connector = HTTPVaultConnector.builder(wireMock.url("/")).withTimeout(250).build();
// Test invalid response code.
final int responseCode = 400;
mockHttpResponse(responseCode, "", "application/json");
VaultConnectorException e = assertThrows(
InvalidResponseException.class,
connector::getHealth,
"Querying health status succeeded on invalid instance"
);
assertEquals("Invalid response code", e.getMessage(), "Unexpected exception message");
assertEquals(responseCode, ((InvalidResponseException) e).getStatusCode(), "Unexpected status code in exception");
assertNull(((InvalidResponseException) e).getResponse(), "Response message where none was expected");
// Simulate permission denied response.
mockHttpResponse(responseCode, "{\"errors\":[\"permission denied\"]}", "application/json");
assertThrows(
PermissionDeniedException.class,
connector::getHealth,
"Querying health status succeeded on invalid instance"
);
// Test exception thrown during request.
try (ServerSocket s = new ServerSocket(0)) {
connector = HTTPVaultConnector.builder("http://localst:" + s.getLocalPort() + "/").withTimeout(250).build();
}
e = assertThrows(
ConnectionException.class,
connector::getHealth,
"Querying health status succeeded on invalid instance"
);
assertEquals("Unable to connect to Vault server", e.getMessage(), "Unexpected exception message");
assertTrue(e.getCause() instanceof IOException, "Unexpected cause");
// Now simulate a failing request that succeeds on second try.
connector = HTTPVaultConnector.builder(wireMock.url("/")).withNumberOfRetries(1).withTimeout(250).build();
WireMock.stubFor(
WireMock.any(anyUrl())
.willReturn(aResponse().withStatus(500))
.willReturn(aResponse().withStatus(500))
.willReturn(aResponse().withStatus(500))
.willReturn(aResponse().withStatus(200).withBody("{}").withHeader("Content-Type", "application/json"))
);
assertDoesNotThrow(connector::getHealth, "Request failed unexpectedly");
}
/**
* Test constructors of the {@link HTTPVaultConnector} class.
*/
@Test
void constructorTest() throws IOException, CertificateException, URISyntaxException {
final String url = "https://vault.example.net/test/";
final String hostname = "vault.example.com";
final Integer port = 1337;
final String prefix = "/custom/prefix/";
final int retries = 42;
final String expectedNoTls = "http://" + hostname + ":8200/v1/";
final String expectedCustomPort = "https://" + hostname + ":" + port + "/v1/";
final String expectedCustomPrefix = "https://" + hostname + ":" + port + prefix;
X509Certificate trustedCaCert;
try (InputStream is = getClass().getResourceAsStream("/tls/ca.pem")) {
trustedCaCert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
}
// Most basic constructor expects complete URL.
HTTPVaultConnector connector = HTTPVaultConnector.builder(url).build();
assertEquals(url, getRequestHelperPrivate(connector, "baseURL"), "Unexpected base URL");
// Now override TLS usage.
connector = HTTPVaultConnector.builder().withHost(hostname).withoutTLS().build();
assertEquals(expectedNoTls, getRequestHelperPrivate(connector, "baseURL"), "Unexpected base URL with TLS disabled");
// Specify custom port.
connector = HTTPVaultConnector.builder().withHost(hostname).withTLS().withPort(port).build();
assertEquals(expectedCustomPort, getRequestHelperPrivate(connector, "baseURL"), "Unexpected base URL with custom port");
// Specify custom prefix.
connector = HTTPVaultConnector.builder().withHost(hostname).withTLS().withPort(port).withPrefix(prefix).build();
assertEquals(expectedCustomPrefix, getRequestHelperPrivate(connector, "baseURL"), "Unexpected base URL with custom prefix");
assertNull(getRequestHelperPrivate(connector, "trustedCaCert"), "Trusted CA cert set, but not specified");
// Specify number of retries.
connector = HTTPVaultConnector.builder(url).withTrustedCA(trustedCaCert).withNumberOfRetries(retries).build();
assertEquals(retries, getRequestHelperPrivate(connector, "retries"), "Number of retries not set correctly");
// Test TLS version (#22).
assertEquals("TLSv1.2", getRequestHelperPrivate(connector, "tlsVersion"), "TLS version should be 1.2 if not specified");
// Now override.
connector = HTTPVaultConnector.builder(url).withTrustedCA(trustedCaCert).withNumberOfRetries(retries).withTLS("TLSv1.1").build();
assertEquals("TLSv1.1", getRequestHelperPrivate(connector, "tlsVersion"), "Overridden TLS version 1.1 not correct");
}
/**
* This test is designed to test exceptions caught and thrown by seal-methods if Vault is not reachable.
*/
@Test
void sealExceptionTest() throws IOException, URISyntaxException {
// Simulate no connection.
VaultConnector connector;
try (ServerSocket s = new ServerSocket(0)) {
connector = HTTPVaultConnector.builder("http://localst:" + s.getLocalPort()).withTimeout(250).build();
}
ConnectionException e = assertThrows(
ConnectionException.class,
connector::sealStatus,
"Querying seal status succeeded on invalid instance"
);
assertEquals("Unable to connect to Vault server", e.getMessage(), "Unexpected exception message");
}
/**
* This test is designed to test exceptions caught and thrown by seal-methods if Vault is not reachable.
*/
@Test
void healthExceptionTest() throws IOException, URISyntaxException {
// Simulate no connection.
HTTPVaultConnector connector;
try (ServerSocket s = new ServerSocket(0)) {
connector = HTTPVaultConnector.builder("http://localhost:" + s.getLocalPort() + "/").withTimeout(250).build();
}
ConnectionException e = assertThrows(
ConnectionException.class,
connector::getHealth,
"Querying health status succeeded on invalid instance"
);
assertEquals("Unable to connect to Vault server", e.getMessage(), "Unexpected exception message");
}
/**
* Test behavior on unparsable responses.
*/
@Test
void parseExceptionTest() throws URISyntaxException {
HTTPVaultConnector connector = HTTPVaultConnector.builder(wireMock.url("/")).withTimeout(250).build();
// Mock authorization.
setPrivate(connector, "authorized", true);
// Mock response.
mockHttpResponse(200, "invalid", "application/json");
// Now test the methods.
assertParseError(connector::sealStatus, "sealStatus() succeeded on invalid instance");
assertParseError(() -> connector.unseal("key"), "unseal() succeeded on invalid instance");
assertParseError(connector::getHealth, "getHealth() succeeded on invalid instance");
assertParseError(connector::getAuthBackends, "getAuthBackends() succeeded on invalid instance");
assertParseError(() -> connector.authToken("token"), "authToken() succeeded on invalid instance");
assertParseError(() -> connector.lookupAppRole("roleName"), "lookupAppRole() succeeded on invalid instance");
assertParseError(() -> connector.getAppRoleID("roleName"), "getAppRoleID() succeeded on invalid instance");
assertParseError(() -> connector.createAppRoleSecret("roleName"), "createAppRoleSecret() succeeded on invalid instance");
assertParseError(() -> connector.lookupAppRoleSecret("roleName", "secretID"), "lookupAppRoleSecret() succeeded on invalid instance");
assertParseError(connector::listAppRoles, "listAppRoles() succeeded on invalid instance");
assertParseError(() -> connector.listAppRoleSecrets("roleName"), "listAppRoleSecrets() succeeded on invalid instance");
assertParseError(() -> connector.read("key"), "read() succeeded on invalid instance");
assertParseError(() -> connector.list("path"), "list() succeeded on invalid instance");
assertParseError(() -> connector.renew("leaseID"), "renew() succeeded on invalid instance");
assertParseError(() -> connector.lookupToken("token"), "lookupToken() succeeded on invalid instance");
}
private void assertParseError(Executable executable, String message) {
InvalidResponseException e = assertThrows(InvalidResponseException.class, executable, message);
assertEquals("Unable to parse response", e.getMessage(), "Unexpected exception message");
}
/**
* Test requests that expect an empty response with code 204, but receive a 200 body.
*/
@Test
void nonEmpty204ResponseTest() throws URISyntaxException {
HTTPVaultConnector connector = HTTPVaultConnector.builder(wireMock.url("/")).withTimeout(250).build();
// Mock authorization.
setPrivate(connector, "authorized", true);
// Mock response.
mockHttpResponse(200, "{}", "application/json");
// Now test the methods expecting a 204.
assertThrows(
InvalidResponseException.class,
() -> connector.registerAppId("appID", "policy", "displayName"),
"registerAppId() with 200 response succeeded"
);
assertThrows(
InvalidResponseException.class,
() -> connector.registerUserId("appID", "userID"),
"registerUserId() with 200 response succeeded"
);
assertThrows(
InvalidResponseException.class,
() -> connector.createAppRole("appID", Collections.singletonList("policy")),
"createAppRole() with 200 response succeeded"
);
assertThrows(
InvalidResponseException.class,
() -> connector.deleteAppRole("roleName"),
"deleteAppRole() with 200 response succeeded"
);
assertThrows(
InvalidResponseException.class,
() -> connector.setAppRoleID("roleName", "roleID"),
"setAppRoleID() with 200 response succeeded"
);
assertThrows(
InvalidResponseException.class,
() -> connector.destroyAppRoleSecret("roleName", "secretID"),
"destroyAppRoleSecret() with 200 response succeeded"
);
assertThrows(
InvalidResponseException.class,
() -> connector.destroyAppRoleSecret("roleName", "secretUD"),
"destroyAppRoleSecret() with 200 response succeeded"
);
assertThrows(
InvalidResponseException.class,
() -> connector.delete("key"),
"delete() with 200 response succeeded"
);
assertThrows(
InvalidResponseException.class,
() -> connector.revoke("leaseID"),
"destroyAppRoleSecret() with 200 response succeeded"
);
}
private Object getRequestHelperPrivate(HTTPVaultConnector connector, String fieldName) {
try {
return getPrivate(getPrivate(connector, "request"), fieldName);
} catch (NoSuchFieldException | IllegalAccessException e) {
return null;
}
}
private Object getPrivate(Object target, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = target.getClass().getDeclaredField(fieldName);
if (field.isAccessible()) {
return field.get(target);
}
field.setAccessible(true);
Object value = field.get(target);
field.setAccessible(false);
return value;
}
private void setPrivate(Object target, String fieldName, Object value) {
try {
Field field = target.getClass().getDeclaredField(fieldName);
boolean accessible = field.isAccessible();
field.setAccessible(true);
field.set(target, value);
field.setAccessible(accessible);
} catch (NoSuchFieldException | IllegalAccessException e) {
// Should not occur, to be taken care of in test code.
}
}
private void mockHttpResponse(int status, String body, String contentType) {
WireMock.stubFor(
WireMock.any(anyUrl()).willReturn(
aResponse().withStatus(status).withBody(body).withHeader("Content-Type", contentType)
)
);
}
}