Skip to content

Commit 4a13ebe

Browse files
authored
VJD-5 Support /sys/wrapping/rewrap endpoint (#17)
1 parent fa3d2b0 commit 4a13ebe

File tree

12 files changed

+581
-210
lines changed

12 files changed

+581
-210
lines changed

.github/workflows/test-report.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ jobs:
1010
steps:
1111
- uses: dorny/test-reporter@v1
1212
with:
13-
artifact: test-results # artifact name
14-
name: Tests vault-java-driver # Name of the check run which will be created
15-
path: '**/*.xml' # Path to test results (inside artifact .zip)
13+
artifact: test-results
14+
name: Tests Report
15+
path: '**/*.xml'
1616
reporter: java-junit
1717
list-suites: 'all'
1818
list-tests: 'all'

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ dependencies {
2525
testCompile('org.bouncycastle:bcpkix-jdk15on:1.70')
2626
testCompile('org.apache.commons:commons-io:1.3.2')
2727

28-
testRuntime('ch.qos.reload4j:reload4j:1.2.22')
28+
testRuntime('org.slf4j:slf4j-simple:2.0.3')
2929
}
3030

3131
// Beginning of Java 9 compatibility config

src/main/java/io/github/jopenlibs/vault/api/Auth.java

Lines changed: 219 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,7 +1282,7 @@ public LookupResponse lookupSelf(final String tokenAuthMount) throws VaultExcept
12821282

12831283
/**
12841284
* <p>Returns information about the current client token for a wrapped token, for which the
1285-
* lookup endpoint is different at "sys/wrapping/lookup". Example usage:</p>
1285+
* lookup endpoint is at "sys/wrapping/lookup". Example usage:</p>
12861286
*
12871287
* <blockquote>
12881288
* <pre>{@code
@@ -1299,23 +1299,85 @@ public LookupResponse lookupSelf(final String tokenAuthMount) throws VaultExcept
12991299
* @throws VaultException If any error occurs, or unexpected response received from Vault
13001300
*/
13011301
public LogicalResponse lookupWrap() throws VaultException {
1302+
return lookupWrap(config.getToken(), false);
1303+
}
1304+
1305+
/**
1306+
* <p>Returns information about the a wrapped token when authorization is needed for lookup,
1307+
* for which the lookup endpoint is at "sys/wrapping/lookup". Example usage:</p>
1308+
*
1309+
* <blockquote>
1310+
* <pre>{@code
1311+
* final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
1312+
* final Vault vault = new Vault(config);
1313+
* ...
1314+
* final String wrappingToken = "...";
1315+
* final LogicalResponse response = vault.auth().lookupWarp(wrappingToken);
1316+
* // Then you can validate "path" for example ...
1317+
* final String path = response.getData().get("path");
1318+
* }</pre>
1319+
* </blockquote>
1320+
*
1321+
* @param wrappedToken Wrapped token.
1322+
* @return The response information returned from Vault
1323+
* @throws VaultException If any error occurs, or unexpected response received from Vault
1324+
*/
1325+
public LogicalResponse lookupWrap(final String wrappedToken) throws VaultException {
1326+
return lookupWrap(wrappedToken, true);
1327+
}
1328+
1329+
/**
1330+
* <p>Returns information about the a wrapped token,
1331+
* for which the lookup endpoint is at "sys/wrapping/lookup". Example usage:</p>
1332+
*
1333+
* <blockquote>
1334+
* <pre>{@code
1335+
* final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
1336+
* final Vault vault = new Vault(config);
1337+
* ...
1338+
* final String wrappingToken = "...";
1339+
* final LogicalResponse response = vault.auth().lookupWarp(wrappingToken);
1340+
* // Then you can validate "path" for example ...
1341+
* final String path = response.getData().get("path");
1342+
* }</pre>
1343+
* </blockquote>
1344+
*
1345+
* @param wrappedToken Wrapped token.
1346+
* @param inBody When {@code true} the token value placed in the body request: {@code {"token": "$wrappedToken"}},
1347+
* otherwise, set the token into header: {@code "X-Vault-Token: $wrappedToken"}.
1348+
* @return The response information returned from Vault
1349+
* @throws VaultException If any error occurs, or unexpected response received from Vault
1350+
*/
1351+
public LogicalResponse lookupWrap(final String wrappedToken, boolean inBody) throws VaultException {
1352+
final String requestJson = inBody ? Json.object().add("token", wrappedToken).toString() : null;
1353+
13021354
return retry(attempt -> {
13031355
// HTTP request to Vault
1304-
final RestResponse restResponse = new Rest()//NOPMD
1356+
Rest rest = new Rest()//NOPMD
13051357
.url(config.getAddress() + "/v1/sys/wrapping/lookup")
1306-
.header("X-Vault-Token", config.getToken())
13071358
.header("X-Vault-Namespace", this.nameSpace)
13081359
.header("X-Vault-Request", "true")
13091360
.connectTimeoutSeconds(config.getOpenTimeout())
13101361
.readTimeoutSeconds(config.getReadTimeout())
13111362
.sslVerification(config.getSslConfig().isVerify())
1312-
.sslContext(config.getSslConfig().getSslContext())
1313-
.get();
1363+
.sslContext(config.getSslConfig().getSslContext());
1364+
1365+
if (inBody) {
1366+
rest = rest
1367+
.header("X-Vault-Token", config.getToken())
1368+
.body(requestJson.getBytes(StandardCharsets.UTF_8));
1369+
} else {
1370+
rest = rest.header("X-Vault-Token", wrappedToken);
1371+
}
1372+
1373+
final RestResponse restResponse = rest.post();
13141374

13151375
// Validate restResponse
13161376
if (restResponse.getStatus() != 200) {
13171377
throw new VaultException(
1318-
"Vault responded with HTTP status code: " + restResponse.getStatus(),
1378+
"Vault responded with HTTP status code: " + restResponse.getStatus() +
1379+
"\nResponse body: " + new String(restResponse.getBody(),
1380+
StandardCharsets.UTF_8),
13191381
restResponse.getStatus());
13201382
}
13211383

@@ -1399,7 +1461,7 @@ public void revokeSelf(final String tokenAuthMount) throws VaultException {
13991461
* @see #unwrap(String)
14001462
*/
14011463
public UnwrapResponse unwrap() throws VaultException {
1402-
return unwrap(null);
1464+
return unwrap(config.getToken(), false);
14031465
}
14041466

14051467
/**
@@ -1452,28 +1514,87 @@ public UnwrapResponse unwrap() throws VaultException {
14521514
* @see #unwrap()
14531515
*/
14541516
public UnwrapResponse unwrap(final String wrappedToken) throws VaultException {
1455-
return retry(attempt -> {
1456-
// Parse parameters to JSON
1457-
final JsonObject jsonObject = Json.object();
1458-
if (wrappedToken != null) {
1459-
jsonObject.add("token", wrappedToken);
1460-
}
1517+
return unwrap(wrappedToken, true);
1518+
}
14611519

1462-
final String requestJson = jsonObject.toString();
1520+
/**
1521+
* <p>Provide access to the {@code /sys/wrapping/unwrap} endpoint.</p>
1522+
*
1523+
* <p>Returns the original response inside the given wrapping token. Unlike simply reading
1524+
* {@code cubbyhole/response} (which is deprecated), this endpoint provides additional
1525+
* validation checks on the token, returns the original value on the wire rather than a JSON
1526+
* string representation of it, and ensures that the response is properly audit-logged.</p>
1527+
*
1528+
* <p> This endpoint can be used by using a wrapping token as the client token in the API call,
1529+
* in which case the token parameter is not required; or, a different token with permissions to
1530+
* access this endpoint can make the call and pass in the wrapping token in the token parameter.
1531+
* Do not use the wrapping token in both locations; this will cause the wrapping token to be
1532+
* revoked but the value to be unable to be looked up, as it will basically be a double-use of
1533+
* the token!</p>
1534+
*
1535+
* <p>In the example below, {@code authToken} is NOT your wrapped token, and should have
1536+
* unwrapping permissions. The unwrapped data in {@link UnwrapResponse#getData()}. Example
1537+
* usage:</p>
1538+
*
1539+
* <blockquote>
1540+
* <pre>{@code
1541+
* final String authToken = "...";
1542+
* final String wrappingToken = "...";
1543+
* final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
1544+
* final Vault vault = new Vault(config);
1545+
*
1546+
* final WrapResponse wrapResponse = vault.auth().wrap(
1547+
* // Data to wrap
1548+
* new JsonObject()
1549+
* .add("foo", "bar")
1550+
* .add("zoo", "zar"),
1551+
*
1552+
* // TTL of the response-wrapping token
1553+
* 60
1554+
* );
1555+
*
1556+
* final UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse.getToken(), true);
1557+
* final JsonObject unwrappedData = response.getData(); // original data
1558+
* }</pre>
1559+
* </blockquote>
1560+
*
1561+
* @param wrappedToken Specifies the wrapping token ID, do NOT also put this in your
1562+
* {@link VaultConfig#getToken()}, if token is {@code null}, this method will unwrap the auth
1563+
* token in {@link VaultConfig#getToken()}
1564+
* @param inBody When {@code true} the token value placed in the body request: {@code {"token": "$wrappedToken"}},
1565+
* otherwise, set the token into header: {@code "X-Vault-Token: $wrappedToken"}.
1566+
* @return The response information returned from Vault
1567+
* @throws VaultException If any error occurs, or unexpected response received from Vault
1568+
* @see #wrap(JsonObject, int)
1569+
* @see #unwrap()
1570+
*/
1571+
public UnwrapResponse unwrap(final String wrappedToken, boolean inBody) throws VaultException {
1572+
Objects.requireNonNull(wrappedToken, "Wrapped token is null");
1573+
1574+
return retry(attempt -> {
14631575
final String url = config.getAddress() + "/v1/sys/wrapping/unwrap";
14641576

14651577
// HTTP request to Vault
1466-
final RestResponse restResponse = new Rest()
1578+
Rest rest = new Rest()
14671579
.url(url)
1468-
.header("X-Vault-Token", config.getToken())
14691580
.header("X-Vault-Namespace", this.nameSpace)
14701581
.header("X-Vault-Request", "true")
1471-
.body(requestJson.getBytes(StandardCharsets.UTF_8))
14721582
.connectTimeoutSeconds(config.getOpenTimeout())
14731583
.readTimeoutSeconds(config.getReadTimeout())
14741584
.sslVerification(config.getSslConfig().isVerify())
1475-
.sslContext(config.getSslConfig().getSslContext())
1476-
.post();
1585+
.sslContext(config.getSslConfig().getSslContext());
1586+
1587+
if (inBody) {
1588+
final String requestJson = Json.object().add("token", wrappedToken).toString();
1589+
rest = rest
1590+
.header("X-Vault-Token", config.getToken())
1591+
.body(requestJson.getBytes(StandardCharsets.UTF_8));
1592+
} else {
1593+
rest = rest
1594+
.header("X-Vault-Token", wrappedToken);
1595+
}
1596+
1597+
RestResponse restResponse = rest.post();
14771598

14781599
// Validate restResponse
14791600
if (restResponse.getStatus() != 200) {
@@ -1560,7 +1681,7 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault
15601681
// HTTP request to Vault
15611682
final RestResponse restResponse = new Rest()
15621683
.url(url)
1563-
.header("X-Vault-Token", config.getToken())
1684+
.header("X-Vault-Token", config.getToken())
15641685
.header("X-Vault-Wrap-TTL", Integer.toString(ttlInSec))
15651686
.header("X-Vault-Namespace", this.nameSpace)
15661687
.header("X-Vault-Request", "true")
@@ -1590,4 +1711,82 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault
15901711
return new WrapResponse(restResponse, attempt);
15911712
});
15921713
}
1714+
1715+
/**
1716+
* <p>Provide access to the {@code /sys/wrapping/rewrap} endpoint. This endpoint rewraps a
1717+
* response-wrapped token. The new token will use the same creation TTL as the original token
1718+
* and contain the same response. The old token will be invalidated. This can be used for
1719+
* long-term storage of a secret in a response-wrapped token when rotation is a
1720+
* requirement.</p>
1721+
*
1722+
* <blockquote>
1723+
* <pre>{@code
1724+
* final String authToken = "...";
1725+
* final String wrappingToken = "...";
1726+
* final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
1727+
* final Vault vault = new Vault(config);
1728+
*
1729+
* final WrapResponse wrapResponse = vault.auth().wrap(
1730+
* // Data to wrap
1731+
* new JsonObject()
1732+
* .add("foo", "bar")
1733+
* .add("zoo", "zar"),
1734+
*
1735+
* // TTL of the response-wrapping token
1736+
* 60
1737+
* );
1738+
* ...
1739+
* final WrapResponse wrapResponse2 = vault.auth().rewrap(wrapResponse.getToken());
1740+
*
1741+
* final UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse2.getToken());
1742+
* final JsonObject unwrappedData = response.getData(); // original data
1743+
* }</pre>
1744+
* </blockquote>
1745+
*
1746+
* @param wrappedToken Wrapped token ID to re-wrap.
1747+
* @return The response information returned from Vault
1748+
* @throws VaultException If any error occurs, or unexpected response received from Vault
1749+
* @see #wrap(JsonObject, int)
1750+
*/
1751+
public WrapResponse rewrap(final String wrappedToken) throws VaultException {
1752+
Objects.requireNonNull(wrappedToken);
1753+
1754+
return retry(attempt -> {
1755+
// Parse parameters to JSON
1756+
final String requestJson = Json.object().add("token", wrappedToken).toString();
1757+
final String url = config.getAddress() + "/v1/sys/wrapping/rewrap";
1758+
1759+
// HTTP request to Vault
1760+
final RestResponse restResponse = new Rest()
1761+
.url(url)
1762+
// .header("X-Vault-Token", wrappedToken)
1763+
.header("X-Vault-Token", config.getToken())
1764+
.header("X-Vault-Namespace", this.nameSpace)
1765+
.header("X-Vault-Request", "true")
1766+
.body(requestJson.getBytes(StandardCharsets.UTF_8))
1767+
.connectTimeoutSeconds(config.getOpenTimeout())
1768+
.readTimeoutSeconds(config.getReadTimeout())
1769+
.sslVerification(config.getSslConfig().isVerify())
1770+
.sslContext(config.getSslConfig().getSslContext())
1771+
.post();
1772+
1773+
// Validate restResponse
1774+
if (restResponse.getStatus() != 200) {
1775+
throw new VaultException(
1776+
"Vault responded with HTTP status code: " + restResponse.getStatus()
1777+
+ "\nResponse body: " + new String(restResponse.getBody(),
1778+
StandardCharsets.UTF_8),
1779+
restResponse.getStatus());
1780+
}
1781+
1782+
final String mimeType =
1783+
restResponse.getMimeType() == null ? "null" : restResponse.getMimeType();
1784+
if (!mimeType.equals("application/json")) {
1785+
throw new VaultException("Vault responded with MIME type: " + mimeType,
1786+
restResponse.getStatus());
1787+
}
1788+
1789+
return new WrapResponse(restResponse, attempt);
1790+
});
1791+
}
15931792
}

src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/WrapUnwrapTests.java renamed to src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/WrappingTests.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.github.jopenlibs.vault.VaultException;
55
import io.github.jopenlibs.vault.json.JsonObject;
66
import io.github.jopenlibs.vault.response.AuthResponse;
7+
import io.github.jopenlibs.vault.response.LogicalResponse;
78
import io.github.jopenlibs.vault.response.UnwrapResponse;
89
import io.github.jopenlibs.vault.response.WrapResponse;
910
import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer;
@@ -13,11 +14,14 @@
1314
import org.junit.Test;
1415

1516
import static org.junit.Assert.assertEquals;
17+
import static org.junit.Assert.assertNotNull;
18+
import static org.junit.Assert.assertThrows;
19+
import static org.junit.Assert.assertTrue;
1620

1721
/**
18-
* Integration tests for the wrap/unwrap data.
22+
* Integration tests for the functions work with {@code /sys/wrapping/*} endpoints.
1923
*/
20-
public class WrapUnwrapTests {
24+
public class WrappingTests {
2125

2226
@ClassRule
2327
public static final VaultContainer container = new VaultContainer();
@@ -27,7 +31,7 @@ public class WrapUnwrapTests {
2731
@BeforeClass
2832
public static void setupClass() throws IOException, InterruptedException, VaultException {
2933
container.initAndUnsealVault();
30-
container.setupBackendUserPass();
34+
container.setupUserPassWithAllowRewrap();
3135

3236
final Vault vault = container.getVault();
3337
final AuthResponse response = vault.auth()
@@ -55,4 +59,39 @@ public void testWrapUnwrap() throws Exception {
5559
assertEquals("bar", unwrapResponse.getData().get("foo").asString());
5660
assertEquals("zar", unwrapResponse.getData().get("zoo").asString());
5761
}
62+
63+
/**
64+
* Tests endpoints: /sys/wrapping/wrap, /sys/wrapping/lookup, /sys/wrapping/unwrap,
65+
* /sys/wrapping/rewrap.
66+
*/
67+
@Test
68+
public void testWrappingAll() throws Exception {
69+
final Vault vault = container.getVault(NONROOT_TOKEN);
70+
71+
WrapResponse wrapResponse0 = vault.auth().wrap(
72+
new JsonObject()
73+
.add("foo", "bar")
74+
.add("zoo", "zar"),
75+
60
76+
);
77+
78+
LogicalResponse look = vault.auth().lookupWrap(wrapResponse0.getToken());
79+
80+
assertNotNull(look.getData().get("creation_time"));
81+
assertNotNull(look.getData().get("creation_ttl"));
82+
assertEquals("sys/wrapping/wrap", look.getData().get("creation_path"));
83+
84+
WrapResponse wrapResponse1 = vault.auth().rewrap(wrapResponse0.getToken());
85+
86+
VaultException ex = assertThrows(
87+
VaultException.class,
88+
() -> vault.auth().unwrap(wrapResponse0.getToken())
89+
);
90+
assertTrue(ex.getMessage().contains("wrapping token is not valid or does not exist"));
91+
92+
UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse1.getToken());
93+
94+
assertEquals("bar", unwrapResponse.getData().get("foo").asString());
95+
assertEquals("zar", unwrapResponse.getData().get("zoo").asString());
96+
}
5897
}

0 commit comments

Comments
 (0)