4 Commits

Author SHA1 Message Date
635cf19e54 prepare release v1.5.3
All checks were successful
CI / build-with-it (11, 1.2.0) (push) Successful in 56s
CI / build-with-it (11, 1.20.3) (push) Successful in 1m12s
CI / build-with-it (17, 1.2.0) (push) Successful in 53s
CI / build-with-it (17, 1.20.3) (push) Successful in 1m6s
CI / build-with-it (21, 1.2.0) (push) Successful in 53s
CI / build-with-it (true, 21, 1.20.3) (push) Successful in 1m1s
2025-09-09 11:47:52 +02:00
f5e40ca032 test: run IT against Vault 1.20.3
All checks were successful
CI / build-with-it (11, 1.2.0) (push) Successful in 54s
CI / build-with-it (11, 1.20.3) (push) Successful in 1m8s
CI / build-with-it (17, 1.2.0) (push) Successful in 50s
CI / build-with-it (17, 1.20.3) (push) Successful in 1m3s
CI / build-with-it (21, 1.2.0) (push) Successful in 49s
CI / build-with-it (true, 21, 1.20.3) (push) Successful in 58s
2025-09-09 11:39:32 +02:00
15f514f877 add token_bound_cidrs field to AppRoleSecret model (#110)
All checks were successful
CI / build-with-it (11, 1.2.0) (push) Successful in 54s
CI / build-with-it (11, 1.20.0) (push) Successful in 1m9s
CI / build-with-it (17, 1.2.0) (push) Successful in 49s
CI / build-with-it (17, 1.20.0) (push) Successful in 1m9s
CI / build-with-it (21, 1.2.0) (push) Successful in 51s
CI / build-with-it (true, 21, 1.20.0) (push) Successful in 56s
2025-09-08 10:25:39 +02:00
f79ed98986 encode user-provided URL parts (#109)
All checks were successful
CI / build-with-it (11, 1.2.0) (push) Successful in 50s
CI / build-with-it (11, 1.20.0) (push) Successful in 1m4s
CI / build-with-it (17, 1.2.0) (push) Successful in 46s
CI / build-with-it (17, 1.20.0) (push) Successful in 1m2s
CI / build-with-it (21, 1.2.0) (push) Successful in 46s
CI / build-with-it (true, 21, 1.20.0) (push) Successful in 54s
In various methods we use user-provided values like role names or lease
ids as parts of the API request path.

Apply URL encoding to these paths that are not expected to contain any
path separators or query args.
2025-09-05 09:46:48 +02:00
10 changed files with 106 additions and 41 deletions

View File

@@ -15,10 +15,10 @@ jobs:
strategy:
matrix:
jdk: [ 11, 17, 21 ]
vault: [ '1.2.0', '1.20.0' ]
vault: [ '1.2.0', '1.20.3' ]
include:
- jdk: 21
vault: '1.20.0'
vault: '1.20.3'
analysis: true
steps:
- name: Checkout

View File

@@ -1,14 +1,17 @@
## unreleased
## 1.5.3 (2025-09-09)
### Dependencies
* Updated Jackson to 2.20.0 (#106)
### Improvements
* Extract API paths into a utility class (#108)
* Encode user-provided URL parts (#109)
* Add `token_bound_cidrs` field to `AppRoleSecret` model (#110)
### Fix
* Prevent potential off-by-1 error in internal `mapOf()` helper (#107)
## 1.5.2 (2025-07-16)
### Dependencies

View File

@@ -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.5.2</version>
<version>1.5.3</version>
</dependency>
```

View File

@@ -3,7 +3,7 @@
<groupId>de.stklcode.jvault</groupId>
<artifactId>jvault-connector</artifactId>
<version>1.5.3-SNAPSHOT</version>
<version>1.5.3</version>
<packaging>jar</packaging>
@@ -32,7 +32,7 @@
<connection>scm:git:git://github.com/stklcode/jvaultconnector.git</connection>
<developerConnection>scm:git:git@github.com:stklcode/jvaultconnector.git</developerConnection>
<url>https://github.com/stklcode/jvaultconnector</url>
<tag>HEAD</tag>
<tag>v1.5.3</tag>
</scm>
<issueManagement>
@@ -42,6 +42,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2025-09-09T09:45:59Z</project.build.outputTimestamp>
<argLine />
</properties>
@@ -178,7 +179,7 @@
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version> 5.2.0.4988</version>
<version>5.2.0.4988</version>
</plugin>
</plugins>
</pluginManagement>

View File

@@ -31,6 +31,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static de.stklcode.jvault.connector.internal.RequestHelper.encode;
import static de.stklcode.jvault.connector.internal.VaultApiPath.*;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
@@ -170,7 +171,7 @@ public class HTTPVaultConnector implements VaultConnector {
public final AuthResponse authUserPass(final String username, final String password)
throws VaultConnectorException {
final Map<String, String> payload = singletonMap("password", password);
return queryAuth(AUTH_USERPASS_LOGIN + username, payload);
return queryAuth(AUTH_USERPASS_LOGIN + encode(username), payload);
}
@Override
@@ -179,7 +180,7 @@ public class HTTPVaultConnector implements VaultConnector {
"role_id", roleID,
"secret_id", secretID
);
return queryAuth(AUTH_APPROLE + LOGIN, payload);
return queryAuth(AUTH_APPROLE + "login", payload);
}
/**
@@ -207,7 +208,7 @@ public class HTTPVaultConnector implements VaultConnector {
requireAuth();
/* Issue request and expect code 204 with empty response */
request.postWithoutResponse(String.format(AUTH_APPROLE_ROLE, role.getName(), ""), role, token);
request.postWithoutResponse(AUTH_APPROLE_ROLE + encode(role.getName()), role, token);
/* Set custom ID if provided */
return !(role.getId() != null && !role.getId().isEmpty()) || setAppRoleID(role.getName(), role.getId());
@@ -218,7 +219,7 @@ public class HTTPVaultConnector implements VaultConnector {
requireAuth();
/* Request HTTP response and parse Secret */
return request.get(
String.format(AUTH_APPROLE_ROLE, roleName, ""),
AUTH_APPROLE_ROLE + encode(roleName),
emptyMap(),
token,
AppRoleResponse.class
@@ -230,7 +231,7 @@ public class HTTPVaultConnector implements VaultConnector {
requireAuth();
/* Issue request and expect code 204 with empty response */
request.deleteWithoutResponse(String.format(AUTH_APPROLE_ROLE, roleName, ""), token);
request.deleteWithoutResponse(AUTH_APPROLE_ROLE + encode(roleName), token);
return true;
}
@@ -240,7 +241,7 @@ public class HTTPVaultConnector implements VaultConnector {
requireAuth();
/* Issue request, parse response and extract Role ID */
return request.get(
String.format(AUTH_APPROLE_ROLE, roleName, "/role-id"),
AUTH_APPROLE_ROLE + encode(roleName) + "/role-id",
emptyMap(),
token,
RawDataResponse.class
@@ -253,7 +254,7 @@ public class HTTPVaultConnector implements VaultConnector {
/* Issue request and expect code 204 with empty response */
request.postWithoutResponse(
String.format(AUTH_APPROLE_ROLE, roleName, "/role-id"),
AUTH_APPROLE_ROLE + encode(roleName) + "/role-id",
singletonMap("role_id", roleID),
token
);
@@ -268,14 +269,14 @@ public class HTTPVaultConnector implements VaultConnector {
if (secret.getId() != null && !secret.getId().isEmpty()) {
return request.post(
String.format(AUTH_APPROLE_ROLE, roleName, "/custom-secret-id"),
AUTH_APPROLE_ROLE + encode(roleName) + "/custom-secret-id",
secret,
token,
AppRoleSecretResponse.class
);
} else {
return request.post(
String.format(AUTH_APPROLE_ROLE, roleName, "/secret-id"),
AUTH_APPROLE_ROLE + encode(roleName) + "/secret-id",
secret, token,
AppRoleSecretResponse.class
);
@@ -289,7 +290,7 @@ public class HTTPVaultConnector implements VaultConnector {
/* Issue request and parse secret response */
return request.post(
String.format(AUTH_APPROLE_ROLE, roleName, "/secret-id/lookup"),
AUTH_APPROLE_ROLE + encode(roleName) + "/secret-id/lookup",
new AppRoleSecret(secretID),
token,
AppRoleSecretResponse.class
@@ -303,7 +304,7 @@ public class HTTPVaultConnector implements VaultConnector {
/* Issue request and expect code 204 with empty response */
request.postWithoutResponse(
String.format(AUTH_APPROLE_ROLE, roleName, "/secret-id/destroy"),
AUTH_APPROLE_ROLE + encode(roleName) + "/secret-id/destroy",
new AppRoleSecret(secretID),
token);
@@ -315,7 +316,7 @@ public class HTTPVaultConnector implements VaultConnector {
requireAuth();
SecretListResponse secrets = request.get(
AUTH_APPROLE + "/role?list=true",
AUTH_APPROLE + "role?list=true",
emptyMap(),
token,
SecretListResponse.class
@@ -329,7 +330,7 @@ public class HTTPVaultConnector implements VaultConnector {
requireAuth();
SecretListResponse secrets = request.get(
String.format(AUTH_APPROLE_ROLE, roleName, "/secret-id?list=true"),
AUTH_APPROLE_ROLE + encode(roleName) + "/secret-id?list=true",
emptyMap(),
token,
SecretListResponse.class
@@ -502,7 +503,7 @@ public class HTTPVaultConnector implements VaultConnector {
requireAuth();
/* Issue request and expect code 204 with empty response */
request.putWithoutResponse(SYS_LEASES_REVOKE + leaseID, emptyMap(), token);
request.putWithoutResponse(SYS_LEASES_REVOKE + encode(leaseID), emptyMap(), token);
}
@Override
@@ -533,7 +534,7 @@ public class HTTPVaultConnector implements VaultConnector {
if (role == null || role.isEmpty()) {
throw new InvalidRequestException("No role name specified.");
}
return createTokenInternal(token, AUTH_TOKEN + TOKEN_CREATE + "/" + role);
return createTokenInternal(token, AUTH_TOKEN + TOKEN_CREATE + "/" + encode(role));
}
@Override
@@ -586,7 +587,7 @@ public class HTTPVaultConnector implements VaultConnector {
}
// Issue request and expect code 204 with empty response.
request.postWithoutResponse(AUTH_TOKEN + TOKEN_ROLES + "/" + name, role, token);
request.postWithoutResponse(AUTH_TOKEN + TOKEN_ROLES + "/" + encode(name), role, token);
return true;
}
@@ -596,7 +597,7 @@ public class HTTPVaultConnector implements VaultConnector {
requireAuth();
// Request HTTP response and parse response.
return request.get(AUTH_TOKEN + TOKEN_ROLES + "/" + name, emptyMap(), token, TokenRoleResponse.class);
return request.get(AUTH_TOKEN + TOKEN_ROLES + "/" + encode(name), emptyMap(), token, TokenRoleResponse.class);
}
@Override
@@ -615,7 +616,7 @@ public class HTTPVaultConnector implements VaultConnector {
}
// Issue request and expect code 204 with empty response.
request.deleteWithoutResponse(AUTH_TOKEN + TOKEN_ROLES + "/" + name, token);
request.deleteWithoutResponse(AUTH_TOKEN + TOKEN_ROLES + "/" + encode(name), token);
return true;
}
@@ -629,7 +630,7 @@ public class HTTPVaultConnector implements VaultConnector {
"plaintext", plaintext
);
return request.post(TRANSIT_ENCRYPT + keyName, payload, token, TransitResponse.class);
return request.post(TRANSIT_ENCRYPT + encode(keyName), payload, token, TransitResponse.class);
}
@Override
@@ -641,7 +642,7 @@ public class HTTPVaultConnector implements VaultConnector {
"ciphertext", ciphertext
);
return request.post(TRANSIT_DECRYPT + keyName, payload, token, TransitResponse.class);
return request.post(TRANSIT_DECRYPT + encode(keyName), payload, token, TransitResponse.class);
}
@Override
@@ -658,7 +659,7 @@ public class HTTPVaultConnector implements VaultConnector {
"format", format
);
return request.post(TRANSIT_HASH + algorithm, payload, token, TransitResponse.class);
return request.post(TRANSIT_HASH + encode(algorithm), payload, token, TransitResponse.class);
}
/**

View File

@@ -25,6 +25,7 @@ import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
@@ -263,9 +264,9 @@ public final class RequestHelper implements Serializable {
if (!payload.isEmpty()) {
uriBuilder.append("?").append(
payload.entrySet().stream().map(par ->
URLEncoder.encode(par.getKey(), UTF_8) + "=" + URLEncoder.encode(par.getValue(), UTF_8)
).collect(Collectors.joining("&"))
payload.entrySet().stream()
.map(par -> encode(par.getKey()) + "=" + encode(par.getValue()))
.collect(Collectors.joining("&"))
);
}
@@ -307,6 +308,17 @@ public final class RequestHelper implements Serializable {
}
}
/**
* Encode URL part.
*
* @param part Path part to URL-encode and insert into the template
* @return Encoded URL part
* @since 1.5.3
*/
public static String encode(final String part) {
return URLEncoder.encode(Objects.requireNonNullElse(part, ""), UTF_8);
}
/**
* Execute prepared HTTP request and return result.
*

View File

@@ -40,8 +40,8 @@ public final class VaultApiPath {
// Auth paths
public static final String AUTH_TOKEN = AUTH + "/token";
public static final String AUTH_USERPASS_LOGIN = AUTH + "/userpass/login/";
public static final String AUTH_APPROLE = AUTH + "/approle";
public static final String AUTH_APPROLE_ROLE = AUTH_APPROLE + "/role/%s%s";
public static final String AUTH_APPROLE = AUTH + "/approle/";
public static final String AUTH_APPROLE_ROLE = AUTH_APPROLE + "role/";
// Token operations
public static final String TOKEN_LOOKUP = "/lookup";
@@ -57,9 +57,6 @@ public final class VaultApiPath {
public static final String SECRET_UNDELETE = "/undelete/";
public static final String SECRET_DESTROY = "/destroy/";
// Generic paths
public static final String LOGIN = "/login";
// Transit engine paths
public static final String TRANSIT_ENCRYPT = TRANSIT + "/encrypt/";
public static final String TRANSIT_DECRYPT = TRANSIT + "/decrypt/";

View File

@@ -32,7 +32,7 @@ import java.util.Objects;
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class AppRoleSecret implements Serializable {
private static final long serialVersionUID = -3401074170145792641L;
private static final long serialVersionUID = 3079272087137299819L;
@JsonProperty("secret_id")
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -47,6 +47,8 @@ public final class AppRoleSecret implements Serializable {
private List<String> cidrList;
private List<String> tokenBoundCidrs;
@JsonProperty(value = "creation_time", access = JsonProperty.Access.WRITE_ONLY)
private String creationTime;
@@ -137,6 +139,36 @@ public final class AppRoleSecret implements Serializable {
return String.join(",", cidrList);
}
/**
* @return list of bound CIDR subnets of associated tokens
* @since 1.5.3
*/
public List<String> getTokenBoundCidrs() {
return tokenBoundCidrs;
}
/**
* @param boundCidrList list of subnets in CIDR notation to bind role to
* @since 1.5.3
*/
@JsonSetter("token_bound_cidrs")
public void setTokenBoundCidrs(final List<String> boundCidrList) {
this.tokenBoundCidrs = boundCidrList;
}
/**
* @return list of subnets in CIDR notation as comma-separated {@link String}
* @since 1.5.3
*/
@JsonGetter("token_bound_cidrs")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public String getTokenBoundCidrsString() {
if (tokenBoundCidrs == null || tokenBoundCidrs.isEmpty()) {
return "";
}
return String.join(",", tokenBoundCidrs);
}
/**
* @return Creation time
*/
@@ -184,6 +216,7 @@ public final class AppRoleSecret implements Serializable {
Objects.equals(accessor, that.accessor) &&
Objects.equals(metadata, that.metadata) &&
Objects.equals(cidrList, that.cidrList) &&
Objects.equals(tokenBoundCidrs, that.tokenBoundCidrs) &&
Objects.equals(creationTime, that.creationTime) &&
Objects.equals(expirationTime, that.expirationTime) &&
Objects.equals(lastUpdatedTime, that.lastUpdatedTime) &&
@@ -193,7 +226,7 @@ public final class AppRoleSecret implements Serializable {
@Override
public int hashCode() {
return Objects.hash(id, accessor, metadata, cidrList, creationTime, expirationTime, lastUpdatedTime, numUses,
ttl);
return Objects.hash(id, accessor, metadata, cidrList, tokenBoundCidrs, creationTime, expirationTime,
lastUpdatedTime, numUses, ttl);
}
}

View File

@@ -52,7 +52,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
* @since 0.1
*/
class HTTPVaultConnectorIT {
private static String VAULT_VERSION = "1.20.0"; // The vault version this test is supposed to run against.
private static String VAULT_VERSION = "1.20.3"; // The vault version this test is supposed to run against.
private static final String KEY1 = "E38bkCm0VhUvpdCKGQpcohhD9XmcHJ/2hreOSY019Lho";
private static final String KEY2 = "O5OHwDleY3IiPdgw61cgHlhsrEm6tVJkrxhF6QAnILd1";
private static final String KEY3 = "mw7Bm3nbt/UWa/juDjjL2EPQ04kiJ0saC5JEXwJvXYsB";

View File

@@ -39,6 +39,7 @@ class AppRoleSecretTest extends AbstractModelTest<AppRoleSecret> {
"number", 1337
);
private static final List<String> TEST_CIDR = List.of("203.0.113.0/24", "198.51.100.0/24");
private static final List<String> TEST_TOKEN_CIDR = List.of("192.0.2.0/24", "198.51.100.0/24");
AppRoleSecretTest() {
super(AppRoleSecret.class);
@@ -61,6 +62,8 @@ class AppRoleSecretTest extends AbstractModelTest<AppRoleSecret> {
assertNull(secret.getMetadata());
assertNull(secret.getCidrList());
assertEquals("", secret.getCidrListString());
assertNull(secret.getTokenBoundCidrs());
assertEquals("", secret.getTokenBoundCidrsString());
assertNull(secret.getCreationTime());
assertNull(secret.getExpirationTime());
assertNull(secret.getLastUpdatedTime());
@@ -74,6 +77,8 @@ class AppRoleSecretTest extends AbstractModelTest<AppRoleSecret> {
assertNull(secret.getMetadata());
assertNull(secret.getCidrList());
assertEquals("", secret.getCidrListString());
assertNull(secret.getTokenBoundCidrs());
assertEquals("", secret.getTokenBoundCidrsString());
assertNull(secret.getCreationTime());
assertNull(secret.getExpirationTime());
assertNull(secret.getLastUpdatedTime());
@@ -87,6 +92,8 @@ class AppRoleSecretTest extends AbstractModelTest<AppRoleSecret> {
assertEquals(TEST_META, secret.getMetadata());
assertEquals(TEST_CIDR, secret.getCidrList());
assertEquals(String.join(",", TEST_CIDR), secret.getCidrListString());
assertNull(secret.getTokenBoundCidrs());
assertEquals("", secret.getTokenBoundCidrsString());
assertNull(secret.getCreationTime());
assertNull(secret.getExpirationTime());
assertNull(secret.getLastUpdatedTime());
@@ -108,6 +115,15 @@ class AppRoleSecretTest extends AbstractModelTest<AppRoleSecret> {
secret.setCidrList(null);
assertNull(secret.getCidrList());
assertEquals("", secret.getCidrListString());
assertNull(secret.getTokenBoundCidrs());
assertEquals("", secret.getTokenBoundCidrsString());
secret.setTokenBoundCidrs(TEST_TOKEN_CIDR);
assertEquals(TEST_TOKEN_CIDR, secret.getTokenBoundCidrs());
assertEquals(String.join(",", TEST_TOKEN_CIDR), secret.getTokenBoundCidrsString());
secret.setTokenBoundCidrs(null);
assertNull(secret.getTokenBoundCidrs());
assertEquals("", secret.getTokenBoundCidrsString());
}
/**
@@ -159,7 +175,8 @@ class AppRoleSecretTest extends AbstractModelTest<AppRoleSecret> {
// Those fields should be deserialized from JSON though.
String secretJson4 = "{\"secret_id\":\"abc123\",\"metadata\":{\"number\":1337,\"foo\":\"bar\"}," +
"\"cidr_list\":[\"203.0.113.0/24\",\"198.51.100.0/24\"],\"secret_id_accessor\":\"TEST_ACCESSOR\"," +
"\"cidr_list\":[\"203.0.113.0/24\",\"198.51.100.0/24\"],\"cidr_list\":[\"192.0.2.0/24\",\"198.51.100.0/24\"]," +
"\"secret_id_accessor\":\"TEST_ACCESSOR\"," +
"\"creation_time\":\"TEST_CREATION\",\"expiration_time\":\"TEST_EXPIRATION\"," +
"\"last_updated_time\":\"TEST_LASTUPDATE\",\"secret_id_num_uses\":678,\"secret_id_ttl\":12345}";
secret2 = assertDoesNotThrow(() -> objectMapper.readValue(secretJson4, AppRoleSecret.class), "Deserialization failed");
@@ -181,6 +198,7 @@ class AppRoleSecretTest extends AbstractModelTest<AppRoleSecret> {
private static String commaSeparatedToList(String json) {
return json.replaceAll("\"cidr_list\":\"([^\"]*)\"", "\"cidr_list\":[$1]")
.replaceAll("\"token_bound_cidrs\":\"([^\"]*)\"", "\"token_bound_cidrs\":[$1]")
.replaceAll("(\\d+\\.\\d+\\.\\d+\\.\\d+/\\d+)", "\"$1\"");
}
}