From a19512b824b670b2423e6f703670ba574552cf3a Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Wed, 5 Feb 2025 11:25:04 +0100 Subject: [PATCH] Added legacy support for submitting keys instead of pubId Signed-off-by: Thomas Schauer-Koeckeis --- .../resources/v1/TeamResource.java | 43 ++++++++---- .../upgrade/v4130/v4130Updater.java | 68 ++++++++++--------- .../resources/v1/TeamResourceTest.java | 56 +++++++++++++++ 3 files changed, 122 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index c59dc6f0b..b9acbd5c4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -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; @@ -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", @@ -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); @@ -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( @@ -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) @@ -360,7 +373,7 @@ public Response updateApiKeyComment( } @DELETE - @Path("/key/{publicId}") + @Path("/key/{publicIdOrKey}") @Operation( summary = "Deletes the specified API key", description = "

Requires permission ACCESS_MANAGEMENT

" @@ -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(); diff --git a/src/main/java/org/dependencytrack/upgrade/v4130/v4130Updater.java b/src/main/java/org/dependencytrack/upgrade/v4130/v4130Updater.java index b3080b4a6..d224a8d39 100644 --- a/src/main/java/org/dependencytrack/upgrade/v4130/v4130Updater.java +++ b/src/main/java/org/dependencytrack/upgrade/v4130/v4130Updater.java @@ -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; @@ -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(); + } } } } diff --git a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index 108467475..c7b2e09f3 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -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() @@ -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() @@ -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()