Skip to content

Commit

Permalink
Added legacy support for submitting keys instead of pubId
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Schauer-Koeckeis <thomas.schauer-koeckeis@rohde-schwarz.com>
  • Loading branch information
Gepardgame committed Feb 5, 2025
1 parent b72ebe6 commit a19512b
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 45 deletions.
43 changes: 31 additions & 12 deletions src/main/java/org/dependencytrack/resources/v1/TeamResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import io.swagger.v3.oas.annotations.tags.Tag;

import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.validation.ValidUuid;
import org.dependencytrack.persistence.QueryManager;
Expand Down Expand Up @@ -287,7 +288,7 @@ public Response generateApiKey(
}

@POST
@Path("/key/{publicId}")
@Path("/key/{publicIdOrKey}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Regenerates an API key by removing the specified key, generating a new one and returning its value",
Expand All @@ -304,10 +305,16 @@ public Response generateApiKey(
})
@PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
public Response regenerateApiKey(
@Parameter(description = "The public ID for the API key to regenerate", required = true)
@PathParam("publicId") String publicId) {
@Parameter(description = "The public ID for the API key or for Legacy the complete Key to regenerate", required = true)
@PathParam("publicIdOrKey") String publicIdOrKey) {
try (QueryManager qm = new QueryManager()) {
ApiKey apiKey = qm.getApiKeyByPublicId(publicId);
boolean isLegacy = publicIdOrKey.length() == ApiKey.LEGACY_FULL_KEY_LENGTH;
ApiKey apiKey;
if (publicIdOrKey.length() == ApiKey.FULL_KEY_LENGTH || isLegacy) {
apiKey = qm.getApiKeyByPublicId(ApiKey.getPublicId(publicIdOrKey, isLegacy));
} else {
apiKey = qm.getApiKeyByPublicId(publicIdOrKey);
}
if (apiKey != null) {
apiKey = qm.regenerateApiKey(apiKey);
apiKey.setLegacy(false);
Expand All @@ -320,7 +327,7 @@ public Response regenerateApiKey(
}

@POST
@Path("/key/{publicId}/comment")
@Path("/key/{publicIdOrKey}/comment")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
Expand All @@ -338,14 +345,20 @@ public Response regenerateApiKey(
})
@PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
public Response updateApiKeyComment(
@Parameter(description = "The public ID for the API key to comment on", required = true)
@PathParam("publicId") final String publicId,
@Parameter(description = "The public ID for the API key or for Legacy the complete Key to comment on", required = true)
@PathParam("publicIdOrKey") final String publicIdOrKey,
final String comment) {
try (final var qm = new QueryManager()) {
qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true");

return qm.callInTransaction(() -> {
final ApiKey apiKey = qm.getApiKeyByPublicId(publicId);
boolean isLegacy = publicIdOrKey.length() == ApiKey.LEGACY_FULL_KEY_LENGTH;
ApiKey apiKey;
if (publicIdOrKey.length() == ApiKey.FULL_KEY_LENGTH || isLegacy) {
apiKey = qm.getApiKeyByPublicId(ApiKey.getPublicId(publicIdOrKey, isLegacy));
} else {
apiKey = qm.getApiKeyByPublicId(publicIdOrKey);
}
if (apiKey == null) {
return Response
.status(Response.Status.NOT_FOUND)
Expand All @@ -360,7 +373,7 @@ public Response updateApiKeyComment(
}

@DELETE
@Path("/key/{publicId}")
@Path("/key/{publicIdOrKey}")
@Operation(
summary = "Deletes the specified API key",
description = "<p>Requires permission <strong>ACCESS_MANAGEMENT</strong></p>"
Expand All @@ -372,10 +385,16 @@ public Response updateApiKeyComment(
})
@PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
public Response deleteApiKey(
@Parameter(description = "The public ID for the API key to delete", required = true)
@PathParam("publicId") String publicId) {
@Parameter(description = "The public ID for the API key or for Legacy the full Key to delete", required = true)
@PathParam("publicIdOrKey") String publicIdOrKey) {
try (QueryManager qm = new QueryManager()) {
final ApiKey apiKey = qm.getApiKeyByPublicId(publicId);
boolean isLegacy = publicIdOrKey.length() == ApiKey.LEGACY_FULL_KEY_LENGTH;
ApiKey apiKey;
if (publicIdOrKey.length() == ApiKey.FULL_KEY_LENGTH || isLegacy) {
apiKey = qm.getApiKeyByPublicId(ApiKey.getPublicId(publicIdOrKey, isLegacy));
} else {
apiKey = qm.getApiKeyByPublicId(publicIdOrKey);
}
if (apiKey != null) {
qm.delete(apiKey);
return Response.status(Response.Status.NO_CONTENT).build();
Expand Down
68 changes: 35 additions & 33 deletions src/main/java/org/dependencytrack/upgrade/v4130/v4130Updater.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.security.MessageDigest;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HexFormat;
Expand Down Expand Up @@ -47,43 +48,44 @@ public void executeUpgrade(final AlpineQueryManager qm, final Connection connect
private void migrateToHashedApiKey(final Connection connection) throws Exception {
LOGGER.info("Store API keys in hashed format!");

final var ps = connection.prepareStatement("""
UPDATE "APIKEY"
SET "APIKEY" = ?, "PUBLIC_ID" = ?, "IS_LEGACY" = ?
WHERE "ID" = ?
""");
try (final PreparedStatement ps = connection.prepareStatement("""
UPDATE "APIKEY"
SET "APIKEY" = ?, "PUBLIC_ID" = ?, "IS_LEGACY" = ?
WHERE "ID" = ?
""")) {

if (DbUtil.isMysql() || DbUtil.isMssql()) {
ps.setInt(3, 1);
} else {
ps.setBoolean(3, true);
}
if (DbUtil.isMysql() || DbUtil.isMssql()) {
ps.setInt(3, 1);
} else {
ps.setBoolean(3, true);
}

try (final Statement statement = connection.createStatement()) {
statement.execute("""
SELECT "ID", "APIKEY"
FROM "APIKEY"
""");
try (final ResultSet rs = statement.getResultSet()) {
String clearKey;
int id;
String hashedKey;
String publicId;
while (rs.next()) {
clearKey = rs.getString("apikey");
if (clearKey.length() != ApiKey.LEGACY_FULL_KEY_LENGTH) {
continue;
}
final MessageDigest digest = MessageDigest.getInstance("SHA3-256");
id = rs.getInt("id");
hashedKey = HexFormat.of().formatHex(digest.digest(ApiKey.getOnlyKeyAsBytes(clearKey, true)));
publicId = ApiKey.getPublicId(clearKey, true);
try (final Statement statement = connection.createStatement()) {
statement.execute("""
SELECT "ID", "APIKEY"
FROM "APIKEY"
""");
try (final ResultSet rs = statement.getResultSet()) {
String clearKey;
int id;
String hashedKey;
String publicId;
while (rs.next()) {
clearKey = rs.getString("apikey");
if (clearKey.length() != ApiKey.LEGACY_FULL_KEY_LENGTH) {
continue;
}
final MessageDigest digest = MessageDigest.getInstance("SHA3-256");
id = rs.getInt("id");
hashedKey = HexFormat.of().formatHex(digest.digest(ApiKey.getOnlyKeyAsBytes(clearKey, true)));
publicId = ApiKey.getPublicId(clearKey, true);

ps.setString(1, hashedKey);
ps.setString(2, publicId);
ps.setInt(4, id);
ps.setString(1, hashedKey);
ps.setString(2, publicId);
ps.setInt(4, id);

ps.executeUpdate();
ps.executeUpdate();
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,21 @@ public void regenerateApiKeyTest() {
Assert.assertEquals(1, team.getApiKeys().size());
}

@Test
public void regenerateApiKeyLegacyTest() {
Team team = qm.createTeam("My Team");
ApiKey apiKey = qm.createApiKey(team);
Assert.assertEquals(1, team.getApiKeys().size());
Response response = jersey.target(V1_TEAM + "/key/" + apiKey.getClearTextKey()).request()
.header(X_API_KEY, apiKey.getClearTextKey())
.post(Entity.entity(null, MediaType.APPLICATION_JSON));
Assert.assertEquals(200, response.getStatus(), 0);
JsonObject json = parseJsonObject(response);
Assert.assertNotNull(json);
Assert.assertNotNull(json.getString("clearTextKey"));
Assert.assertEquals(1, team.getApiKeys().size());
}

@Test
public void regenerateApiKeyInvalidTest() {
Response response = jersey.target(V1_TEAM + "/key/" + UUID.randomUUID().toString()).request()
Expand All @@ -350,6 +365,17 @@ public void deleteApiKeyTest() {
Assert.assertEquals(204, response.getStatus(), 0);
}

@Test
public void deleteApiKeyLegacyTest() {
Team team = qm.createTeam("My Team");
ApiKey apiKey = qm.createApiKey(team);
Assert.assertEquals(1, team.getApiKeys().size());
Response response = jersey.target(V1_TEAM + "/key/" + apiKey.getClearTextKey()).request()
.header(X_API_KEY, apiKey.getClearTextKey())
.delete();
Assert.assertEquals(204, response.getStatus(), 0);
}

@Test
public void deleteApiKeyInvalidTest() {
Response response = jersey.target(V1_TEAM + "/key/" + UUID.randomUUID().toString()).request()
Expand Down Expand Up @@ -391,6 +417,36 @@ public void updateApiKeyCommentTest() {
""");
}

@Test
public void updateApiKeyCommentLegacyTest() {
final Team team = qm.createTeam("foo");
final ApiKey apiKey = qm.createApiKey(team);

assertThat(apiKey.getCreated()).isNotNull();
assertThat(apiKey.getLastUsed()).isNull();
assertThat(apiKey.getComment()).isNull();

final Response response = jersey.target("%s/key/%s/comment".formatted(V1_TEAM, apiKey.getClearTextKey())).request()
.header(X_API_KEY, this.apiKey)
.post(Entity.entity("Some comment 123", MediaType.TEXT_PLAIN));

assertThat(response.getStatus()).isEqualTo(200);
assertThatJson(getPlainTextBody(response))
.withMatcher("publicId", equalTo(apiKey.getPublicId()))
.withMatcher("maskedKey", equalTo(apiKey.getMaskedKey()))
.isEqualTo("""
{
"publicId": "${json-unit.matches:publicId}",
"clearTextKey":null,
"maskedKey": "${json-unit.matches:maskedKey}",
"created": "${json-unit.any-number}",
"lastUsed": null,
"legacy":false,
"comment": "Some comment 123"
}
""");
}

@Test
public void updateApiKeyCommentNotFoundTest() {
final Response response = jersey.target("%s/key/does-not-exist/comment".formatted(V1_TEAM)).request()
Expand Down

0 comments on commit a19512b

Please sign in to comment.