diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKey.java b/apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKey.java index 6514eca34e..943ce83661 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKey.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKey.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Builder; import java.time.Instant; @@ -22,8 +23,9 @@ public record ProviderApiKey( @JsonView( { View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) UUID id, @JsonView({View.Public.class, View.Write.class}) @NotNull LlmProvider provider, - @JsonView({ + @JsonView({View.Public.class, View.Write.class}) @NotBlank @JsonDeserialize(using = ProviderApiKeyDeserializer.class) String apiKey, + @JsonView({View.Public.class, View.Write.class}) @Size(max = 150) String name, @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant createdAt, @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String createdBy, @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant lastUpdatedAt, @@ -49,6 +51,7 @@ public static class Public { } } + @Builder(toBuilder = true) public record ProviderApiKeyPage( @JsonView( { Project.View.Public.class}) int page, diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKeyUpdate.java b/apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKeyUpdate.java index 518d23a8dd..4e351dffd9 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKeyUpdate.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/ProviderApiKeyUpdate.java @@ -6,13 +6,15 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.Builder; @Builder(toBuilder = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record ProviderApiKeyUpdate( - @NotBlank @JsonDeserialize(using = ProviderApiKeyDeserializer.class) String apiKey) { + @NotBlank @JsonDeserialize(using = ProviderApiKeyDeserializer.class) String apiKey, + @Size(max = 150) String name) { @Override public String toString() { diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResource.java b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResource.java index eba7ef04d9..d5825bac64 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResource.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResource.java @@ -38,6 +38,9 @@ import java.util.UUID; +import static com.comet.opik.infrastructure.EncryptionUtils.decrypt; +import static com.comet.opik.infrastructure.EncryptionUtils.maskApiKey; + @Path("/v1/private/llm-provider-key") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @@ -60,10 +63,21 @@ public Response find() { String workspaceId = requestContext.get().getWorkspaceId(); log.info("Find LLM Provider's ApiKeys for workspaceId '{}'", workspaceId); - Page providerApiKeyPage = llmProviderApiKeyService.find(workspaceId); + ProviderApiKey.ProviderApiKeyPage providerApiKeyPage = llmProviderApiKeyService.find(workspaceId); log.info("Found LLM Provider's ApiKeys for workspaceId '{}'", workspaceId); - return Response.ok().entity(providerApiKeyPage).build(); + return Response.ok().entity( + providerApiKeyPage.toBuilder() + .content( + providerApiKeyPage.content().stream() + .map(providerApiKey -> providerApiKey.toBuilder() + .apiKey(maskApiKey(decrypt(providerApiKey.apiKey()))) + .build()) + .toList() + ) + .build() + ) + .build(); } @GET @@ -82,7 +96,9 @@ public Response getById(@PathParam("id") UUID id) { log.info("Got LLM Provider's ApiKey by id '{}' on workspace_id '{}'", id, workspaceId); - return Response.ok().entity(providerApiKey).build(); + return Response.ok().entity(providerApiKey.toBuilder() + .apiKey(maskApiKey(decrypt(providerApiKey.apiKey()))) + .build()).build(); } @POST diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyDAO.java index 9a4586cdd7..515cd840ec 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyDAO.java @@ -1,6 +1,7 @@ package com.comet.opik.domain; import com.comet.opik.api.ProviderApiKey; +import com.comet.opik.api.ProviderApiKeyUpdate; import com.comet.opik.infrastructure.db.UUIDArgumentFactory; import org.jdbi.v3.sqlobject.config.RegisterArgumentFactory; import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper; @@ -19,18 +20,18 @@ @RegisterArgumentFactory(UUIDArgumentFactory.class) public interface LlmProviderApiKeyDAO { - @SqlUpdate("INSERT INTO llm_provider_api_key (id, provider, workspace_id, api_key, created_by, last_updated_by) VALUES (:bean.id, :bean.provider, :workspaceId, :bean.apiKey, :bean.createdBy, :bean.lastUpdatedBy)") + @SqlUpdate("INSERT INTO llm_provider_api_key (id, provider, workspace_id, api_key, name, created_by, last_updated_by) " + + "VALUES (:bean.id, :bean.provider, :workspaceId, :bean.apiKey, :bean.name, :bean.createdBy, :bean.lastUpdatedBy)") void save(@Bind("workspaceId") String workspaceId, @BindMethods("bean") ProviderApiKey providerApiKey); @SqlUpdate("UPDATE llm_provider_api_key SET " + - "api_key = :apiKey, " + - "last_updated_by = :lastUpdatedBy " + + "api_key = :bean.apiKey, name = :bean.name, last_updated_by = :lastUpdatedBy " + "WHERE id = :id AND workspace_id = :workspaceId") void update(@Bind("id") UUID id, @Bind("workspaceId") String workspaceId, - @Bind("apiKey") String encryptedApiKey, - @Bind("lastUpdatedBy") String lastUpdatedBy); + @Bind("lastUpdatedBy") String lastUpdatedBy, + @BindMethods("bean") ProviderApiKeyUpdate providerApiKeyUpdate); @SqlQuery("SELECT * FROM llm_provider_api_key WHERE id = :id AND workspace_id = :workspaceId") ProviderApiKey findById(@Bind("id") UUID id, @Bind("workspaceId") String workspaceId); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyService.java index 3d99d09320..d75edf109d 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyService.java @@ -30,7 +30,7 @@ public interface LlmProviderApiKeyService { ProviderApiKey find(UUID id, String workspaceId); - Page find(String workspaceId); + ProviderApiKey.ProviderApiKeyPage find(String workspaceId); ProviderApiKey saveApiKey(ProviderApiKey providerApiKey, String userName, String workspaceId); @@ -58,12 +58,11 @@ public ProviderApiKey find(@NonNull UUID id, @NonNull String workspaceId) { return repository.fetch(id, workspaceId).orElseThrow(this::createNotFoundError); }); - return providerApiKey.toBuilder() - .build(); + return providerApiKey; } @Override - public Page find(@NonNull String workspaceId) { + public ProviderApiKey.ProviderApiKeyPage find(@NonNull String workspaceId) { List providerApiKeys = template.inTransaction(READ_ONLY, handle -> { var repository = handle.attach(LlmProviderApiKeyDAO.class); return repository.find(workspaceId); @@ -122,8 +121,8 @@ public void updateApiKey(@NonNull UUID id, @NonNull ProviderApiKeyUpdate provide repository.update(providerApiKey.id(), workspaceId, - providerApiKeyUpdate.apiKey(), - userName); + userName, + providerApiKeyUpdate); return null; }); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/infrastructure/EncryptionUtils.java b/apps/opik-backend/src/main/java/com/comet/opik/infrastructure/EncryptionUtils.java index 7bbb9910de..243323f31d 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/infrastructure/EncryptionUtils.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/infrastructure/EncryptionUtils.java @@ -1,6 +1,7 @@ package com.comet.opik.infrastructure; import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -50,4 +51,11 @@ public static String decrypt(@NonNull String encryptedData) { throw new SecurityException("Failed to decrypt. " + ex.getMessage(), ex); } } + + public static String maskApiKey(@NonNull String apiKey) { + return apiKey.length() <= 12 + ? StringUtils.repeat('*', apiKey.length()) + : apiKey.substring(0, 3) + StringUtils.repeat('*', apiKey.length() - 6) + + apiKey.substring(apiKey.length() - 3); + } } diff --git a/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_name_to_provider_api_key.sql b/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_name_to_provider_api_key.sql new file mode 100644 index 0000000000..3e3d6a3e6f --- /dev/null +++ b/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_name_to_provider_api_key.sql @@ -0,0 +1,6 @@ +--liquibase formatted sql +--changeset BorisTkachenko:000008_add_name_to_provider_api_key + +ALTER TABLE llm_provider_api_key ADD COLUMN name VARCHAR(150) DEFAULT NULL; + +--rollback ALTER TABLE llm_provider_api_key DROP COLUMN name; diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/resources/LlmProviderApiKeyResourceClient.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/resources/LlmProviderApiKeyResourceClient.java index c5d15559e2..06c846a363 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/resources/LlmProviderApiKeyResourceClient.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/resources/LlmProviderApiKeyResourceClient.java @@ -11,7 +11,6 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import ru.vyarus.dropwizard.guice.test.ClientSupport; -import uk.co.jemos.podam.api.PodamUtils; import java.util.Set; import java.util.UUID; @@ -31,23 +30,17 @@ public LlmProviderApiKeyResourceClient(ClientSupport client) { } public ProviderApiKey createProviderApiKey( - String providerApiKey, String apiKey, String workspaceName, int expectedStatus) { - return createProviderApiKey(providerApiKey, randomLlmProvider(), apiKey, workspaceName, expectedStatus); - } - - public ProviderApiKey createProviderApiKey( - String providerApiKey, LlmProvider llmProvider, String apiKey, String workspaceName, int expectedStatus) { - ProviderApiKey body = ProviderApiKey.builder().provider(llmProvider).apiKey(providerApiKey).build(); + ProviderApiKey providerApiKey, String apiKey, String workspaceName, int expectedStatus) { try (var actualResponse = client.target(RESOURCE_PATH.formatted(baseURI)) .request() .accept(MediaType.APPLICATION_JSON_TYPE) .header(HttpHeaders.AUTHORIZATION, apiKey) .header(WORKSPACE_HEADER, workspaceName) - .post(Entity.json(body))) { + .post(Entity.json(providerApiKey))) { assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(expectedStatus); if (expectedStatus == 201) { - return body.toBuilder() + return providerApiKey.toBuilder() .id(TestUtils.getIdFromLocation(actualResponse.getLocation())) .build(); } @@ -56,7 +49,15 @@ public ProviderApiKey createProviderApiKey( } } - public void updateProviderApiKey(UUID id, String providerApiKey, String apiKey, String workspaceName, + public ProviderApiKey createProviderApiKey( + String providerApiKey, LlmProvider llmProvider, String apiKey, String workspaceName, int expectedStatus) { + ProviderApiKey body = ProviderApiKey.builder().provider(llmProvider).apiKey(providerApiKey).build(); + + return createProviderApiKey(body, apiKey, workspaceName, expectedStatus); + } + + public void updateProviderApiKey(UUID id, ProviderApiKeyUpdate providerApiKeyUpdate, String apiKey, + String workspaceName, int expectedStatus) { try (var actualResponse = client.target(RESOURCE_PATH.formatted(baseURI)) .path(id.toString()) @@ -64,7 +65,7 @@ public void updateProviderApiKey(UUID id, String providerApiKey, String apiKey, .accept(MediaType.APPLICATION_JSON_TYPE) .header(HttpHeaders.AUTHORIZATION, apiKey) .header(WORKSPACE_HEADER, workspaceName) - .method(HttpMethod.PATCH, Entity.json(ProviderApiKeyUpdate.builder().apiKey(providerApiKey).build()))) { + .method(HttpMethod.PATCH, Entity.json(providerApiKeyUpdate))) { assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(expectedStatus); } @@ -95,10 +96,7 @@ public ProviderApiKey getById(UUID id, String workspaceName, String apiKey, int if (expectedStatus == 200) { assertThat(actualResponse.hasEntity()).isTrue(); - var actualEntity = actualResponse.readEntity(ProviderApiKey.class); - assertThat(actualEntity.apiKey()).isBlank(); - - return actualEntity; + return actualResponse.readEntity(ProviderApiKey.class); } return null; @@ -116,13 +114,8 @@ public Page getAll(String workspaceName, String apiKey) { assertThat(actualResponse.hasEntity()).isTrue(); var actualEntity = actualResponse.readEntity(ProviderApiKey.ProviderApiKeyPage.class); - actualEntity.content().forEach(providerApiKey -> assertThat(providerApiKey.apiKey()).isBlank()); return actualEntity; } } - - public LlmProvider randomLlmProvider() { - return LlmProvider.values()[PodamUtils.getIntegerInRange(0, LlmProvider.values().length - 1)]; - } } diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResourceTest.java index db4b1bfd1e..c0e2814dd4 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResourceTest.java @@ -2,6 +2,7 @@ import com.comet.opik.api.Page; import com.comet.opik.api.ProviderApiKey; +import com.comet.opik.api.ProviderApiKeyUpdate; import com.comet.opik.api.resources.utils.AuthTestUtils; import com.comet.opik.api.resources.utils.ClickHouseContainerUtils; import com.comet.opik.api.resources.utils.ClientSupportUtils; @@ -13,9 +14,9 @@ import com.comet.opik.api.resources.utils.resources.LlmProviderApiKeyResourceClient; import com.comet.opik.domain.LlmProviderApiKeyDAO; import com.comet.opik.infrastructure.DatabaseAnalyticsFactory; -import com.comet.opik.infrastructure.EncryptionUtils; import com.comet.opik.podam.PodamFactoryUtils; import com.redis.testcontainers.RedisContainer; +import org.apache.commons.lang3.StringUtils; import org.jdbi.v3.core.Jdbi; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -38,6 +39,8 @@ import static com.comet.opik.api.resources.utils.ClickHouseContainerUtils.DATABASE_NAME; import static com.comet.opik.api.resources.utils.MigrationUtils.CLICKHOUSE_CHANGELOG_FILE; +import static com.comet.opik.infrastructure.EncryptionUtils.decrypt; +import static com.comet.opik.infrastructure.EncryptionUtils.maskApiKey; import static com.comet.opik.infrastructure.db.TransactionTemplateAsync.READ_ONLY; import static org.assertj.core.api.Assertions.assertThat; @@ -109,19 +112,52 @@ void createAndUpdateProviderApiKey() { String workspaceName = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); - String providerApiKey = factory.manufacturePojo(String.class); + ProviderApiKey providerApiKey = factory.manufacturePojo(ProviderApiKey.class); mockTargetWorkspace(apiKey, workspaceName, workspaceId); var expectedProviderApiKey = llmProviderApiKeyResourceClient.createProviderApiKey(providerApiKey, apiKey, workspaceName, 201); getAndAssertProviderApiKey(expectedProviderApiKey, apiKey, workspaceName); - checkEncryption(expectedProviderApiKey.id(), workspaceId, providerApiKey); + checkEncryption(expectedProviderApiKey.id(), workspaceId, providerApiKey.apiKey()); - String newProviderApiKey = factory.manufacturePojo(String.class); - llmProviderApiKeyResourceClient.updateProviderApiKey(expectedProviderApiKey.id(), newProviderApiKey, apiKey, + var providerApiKeyUpdate = factory.manufacturePojo(ProviderApiKeyUpdate.class); + llmProviderApiKeyResourceClient.updateProviderApiKey(expectedProviderApiKey.id(), providerApiKeyUpdate, apiKey, workspaceName, 204); - checkEncryption(expectedProviderApiKey.id(), workspaceId, newProviderApiKey); + + var expectedUpdatedProviderApiKey = expectedProviderApiKey.toBuilder() + .apiKey(providerApiKeyUpdate.apiKey()) + .name(providerApiKeyUpdate.name()) + .build(); + getAndAssertProviderApiKey(expectedUpdatedProviderApiKey, apiKey, workspaceName); + + checkEncryption(expectedProviderApiKey.id(), workspaceId, providerApiKeyUpdate.apiKey()); + } + + @Test + @DisplayName("Create and update provider Api Key for invalid name") + void createAndUpdateProviderApiKeyForInvalidName() { + + String workspaceName = UUID.randomUUID().toString(); + String apiKey = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + + mockTargetWorkspace(apiKey, workspaceName, workspaceId); + + ProviderApiKey invalidNameProviderApiKey = factory.manufacturePojo(ProviderApiKey.class).toBuilder() + .name(StringUtils.repeat('x', 160)) + .build(); + llmProviderApiKeyResourceClient.createProviderApiKey(invalidNameProviderApiKey, apiKey, workspaceName, 422); + + ProviderApiKey providerApiKey = factory.manufacturePojo(ProviderApiKey.class); + var expectedProviderApiKey = llmProviderApiKeyResourceClient.createProviderApiKey(providerApiKey, apiKey, + workspaceName, 201); + + var providerApiKeyUpdate = factory.manufacturePojo(ProviderApiKeyUpdate.class).toBuilder() + .name(StringUtils.repeat('x', 160)) + .build(); + llmProviderApiKeyResourceClient.updateProviderApiKey(expectedProviderApiKey.id(), providerApiKeyUpdate, apiKey, + workspaceName, 422); } @Test @@ -131,7 +167,7 @@ void createAndBatchDeleteProviderApiKeys() { String workspaceName = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); - String providerApiKey = factory.manufacturePojo(String.class); + ProviderApiKey providerApiKey = factory.manufacturePojo(ProviderApiKey.class); mockTargetWorkspace(apiKey, workspaceName, workspaceId); @@ -157,7 +193,7 @@ void createProviderApiKeyForExistingProviderShouldFail() { String workspaceName = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); - String providerApiKey = factory.manufacturePojo(String.class); + ProviderApiKey providerApiKey = factory.manufacturePojo(ProviderApiKey.class); mockTargetWorkspace(apiKey, workspaceName, workspaceId); @@ -172,12 +208,13 @@ void updateProviderFail() { String workspaceName = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); - String providerApiKey = factory.manufacturePojo(String.class); mockTargetWorkspace(apiKey, workspaceName, workspaceId); + var providerApiKeyUpdate = factory.manufacturePojo(ProviderApiKeyUpdate.class); // for non-existing id - llmProviderApiKeyResourceClient.updateProviderApiKey(UUID.randomUUID(), providerApiKey, apiKey, workspaceName, + llmProviderApiKeyResourceClient.updateProviderApiKey(UUID.randomUUID(), providerApiKeyUpdate, apiKey, + workspaceName, 404); } @@ -188,7 +225,7 @@ void createAndGetProviderApiKeyList() { String workspaceName = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); - String providerApiKey = factory.manufacturePojo(String.class); + ProviderApiKey providerApiKey = factory.manufacturePojo(ProviderApiKey.class); mockTargetWorkspace(apiKey, workspaceName, workspaceId); @@ -201,13 +238,15 @@ void createAndGetProviderApiKeyList() { workspaceName, 201); actualProviderApiKeyPage = llmProviderApiKeyResourceClient.getAll(workspaceName, apiKey); assertPage(actualProviderApiKeyPage, List.of(expectedProviderApiKey)); - } private void getAndAssertProviderApiKey(ProviderApiKey expected, String apiKey, String workspaceName) { var actualEntity = llmProviderApiKeyResourceClient.getById(expected.id(), workspaceName, apiKey, 200); assertThat(actualEntity.provider()).isEqualTo(expected.provider()); - assertThat(actualEntity.apiKey()).isBlank(); + assertThat(actualEntity.name()).isEqualTo(expected.name()); + + // We should decrypt api key in order to compare, since it encrypts on deserialization + assertThat(decrypt(actualEntity.apiKey())).isEqualTo(maskApiKey(expected.apiKey())); } private void checkEncryption(UUID id, String workspaceId, String expectedApiKey) { @@ -215,7 +254,7 @@ private void checkEncryption(UUID id, String workspaceId, String expectedApiKey) var repository = handle.attach(LlmProviderApiKeyDAO.class); return repository.findById(id, workspaceId).apiKey(); }); - assertThat(EncryptionUtils.decrypt(actualEncryptedApiKey)).isEqualTo(expectedApiKey); + assertThat(decrypt(actualEncryptedApiKey)).isEqualTo(expectedApiKey); } private void assertPage(Page actual, List expected) { @@ -224,7 +263,11 @@ private void assertPage(Page actual, List expect assertThat(actual.total()).isEqualTo(expected.size()); assertThat(actual.size()).isEqualTo(expected.size()); - assertThat(actual.content().stream().map(ProviderApiKey::provider).toList()) - .isEqualTo(expected.stream().map(ProviderApiKey::provider).toList()); + for (int i = 0; i < expected.size(); i++) { + assertThat(actual.content().get(i).provider()).isEqualTo(expected.get(i).provider()); + assertThat(actual.content().get(i).name()).isEqualTo(expected.get(i).name()); + assertThat(decrypt(actual.content().get(i).apiKey())) + .isEqualTo(maskApiKey(expected.get(i).apiKey())); + } } } diff --git a/apps/opik-backend/src/test/java/com/comet/opik/podam/PodamFactoryUtils.java b/apps/opik-backend/src/test/java/com/comet/opik/podam/PodamFactoryUtils.java index 6e632739ce..b86d77d830 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/podam/PodamFactoryUtils.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/podam/PodamFactoryUtils.java @@ -2,12 +2,16 @@ import com.comet.opik.api.DatasetItem; import com.comet.opik.api.PromptVersion; +import com.comet.opik.api.ProviderApiKey; +import com.comet.opik.api.ProviderApiKeyUpdate; import com.comet.opik.podam.manufacturer.BigDecimalTypeManufacturer; import com.comet.opik.podam.manufacturer.CategoricalFeedbackDetailTypeManufacturer; import com.comet.opik.podam.manufacturer.DatasetItemTypeManufacturer; import com.comet.opik.podam.manufacturer.JsonNodeTypeManufacturer; import com.comet.opik.podam.manufacturer.NumericalFeedbackDetailTypeManufacturer; import com.comet.opik.podam.manufacturer.PromptVersionManufacturer; +import com.comet.opik.podam.manufacturer.ProviderApiKeyManufacturer; +import com.comet.opik.podam.manufacturer.ProviderApiKeyUpdateManufacturer; import com.comet.opik.podam.manufacturer.UUIDTypeManufacturer; import com.fasterxml.jackson.databind.JsonNode; import jakarta.validation.constraints.DecimalMax; @@ -44,6 +48,8 @@ public static PodamFactory newPodamFactory() { strategy.addOrReplaceTypeManufacturer(JsonNode.class, JsonNodeTypeManufacturer.INSTANCE); strategy.addOrReplaceTypeManufacturer(DatasetItem.class, DatasetItemTypeManufacturer.INSTANCE); strategy.addOrReplaceTypeManufacturer(PromptVersion.class, PromptVersionManufacturer.INSTANCE); + strategy.addOrReplaceTypeManufacturer(ProviderApiKey.class, ProviderApiKeyManufacturer.INSTANCE); + strategy.addOrReplaceTypeManufacturer(ProviderApiKeyUpdate.class, ProviderApiKeyUpdateManufacturer.INSTANCE); return podamFactory; } diff --git a/apps/opik-backend/src/test/java/com/comet/opik/podam/manufacturer/ProviderApiKeyManufacturer.java b/apps/opik-backend/src/test/java/com/comet/opik/podam/manufacturer/ProviderApiKeyManufacturer.java new file mode 100644 index 0000000000..1f2556927f --- /dev/null +++ b/apps/opik-backend/src/test/java/com/comet/opik/podam/manufacturer/ProviderApiKeyManufacturer.java @@ -0,0 +1,37 @@ +package com.comet.opik.podam.manufacturer; + +import com.comet.opik.api.LlmProvider; +import com.comet.opik.api.ProviderApiKey; +import org.apache.commons.lang3.RandomStringUtils; +import uk.co.jemos.podam.api.AttributeMetadata; +import uk.co.jemos.podam.api.DataProviderStrategy; +import uk.co.jemos.podam.api.PodamUtils; +import uk.co.jemos.podam.common.ManufacturingContext; +import uk.co.jemos.podam.typeManufacturers.AbstractTypeManufacturer; + +import java.time.Instant; +import java.util.UUID; + +public class ProviderApiKeyManufacturer extends AbstractTypeManufacturer { + public static final ProviderApiKeyManufacturer INSTANCE = new ProviderApiKeyManufacturer(); + + @Override + public ProviderApiKey getType(DataProviderStrategy strategy, AttributeMetadata metadata, + ManufacturingContext context) { + + UUID id = strategy.getTypeValue(metadata, context, UUID.class); + + return ProviderApiKey.builder() + .id(id) + .provider(randomLlmProvider()) + .apiKey(RandomStringUtils.randomAlphanumeric(20)) + .name(strategy.getTypeValue(metadata, context, String.class)) + .createdBy(strategy.getTypeValue(metadata, context, String.class)) + .createdAt(Instant.now()) + .build(); + } + + public LlmProvider randomLlmProvider() { + return LlmProvider.values()[PodamUtils.getIntegerInRange(0, LlmProvider.values().length - 1)]; + } +} diff --git a/apps/opik-backend/src/test/java/com/comet/opik/podam/manufacturer/ProviderApiKeyUpdateManufacturer.java b/apps/opik-backend/src/test/java/com/comet/opik/podam/manufacturer/ProviderApiKeyUpdateManufacturer.java new file mode 100644 index 0000000000..ec05894176 --- /dev/null +++ b/apps/opik-backend/src/test/java/com/comet/opik/podam/manufacturer/ProviderApiKeyUpdateManufacturer.java @@ -0,0 +1,21 @@ +package com.comet.opik.podam.manufacturer; + +import com.comet.opik.api.ProviderApiKeyUpdate; +import org.apache.commons.lang3.RandomStringUtils; +import uk.co.jemos.podam.api.AttributeMetadata; +import uk.co.jemos.podam.api.DataProviderStrategy; +import uk.co.jemos.podam.common.ManufacturingContext; +import uk.co.jemos.podam.typeManufacturers.AbstractTypeManufacturer; + +public class ProviderApiKeyUpdateManufacturer extends AbstractTypeManufacturer { + public static final ProviderApiKeyUpdateManufacturer INSTANCE = new ProviderApiKeyUpdateManufacturer(); + + @Override + public ProviderApiKeyUpdate getType(DataProviderStrategy strategy, AttributeMetadata metadata, + ManufacturingContext context) { + + return new ProviderApiKeyUpdate( + RandomStringUtils.randomAlphanumeric(20), + strategy.getTypeValue(metadata, context, String.class)); + } +}