Skip to content

Commit

Permalink
[OPIK-548] List LLM Provider api keys (#864)
Browse files Browse the repository at this point in the history
  • Loading branch information
BorisTkachenko authored Dec 12, 2024
1 parent eef2c52 commit 363b14f
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.comet.opik.api.error.JsonInvalidFormatExceptionMapper;
import com.comet.opik.infrastructure.ConfigurationModule;
import com.comet.opik.infrastructure.EncryptionUtilsModule;
import com.comet.opik.infrastructure.EncryptionUtils;
import com.comet.opik.infrastructure.OpikConfiguration;
import com.comet.opik.infrastructure.auth.AuthModule;
import com.comet.opik.infrastructure.bi.BiModule;
Expand Down Expand Up @@ -70,7 +70,7 @@ public void initialize(Bootstrap<OpikConfiguration> bootstrap) {
.withPlugins(new SqlObjectPlugin(), new Jackson2Plugin()))
.modules(new DatabaseAnalyticsModule(), new IdGeneratorModule(), new AuthModule(), new RedisModule(),
new RateLimitModule(), new NameGeneratorModule(), new HttpModule(), new EventModule(),
new ConfigurationModule(), new BiModule(), new EncryptionUtilsModule())
new ConfigurationModule(), new BiModule())
.installers(JobGuiceyInstaller.class)
.listen(new OpikGuiceyLifecycleEventListener())
.enableAutoConfig()
Expand All @@ -79,6 +79,7 @@ public void initialize(Bootstrap<OpikConfiguration> bootstrap) {

@Override
public void run(OpikConfiguration configuration, Environment environment) {
EncryptionUtils.setConfig(configuration);
// Resources
var jersey = environment.jersey();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import lombok.NonNull;

import java.time.Instant;
import java.util.List;
import java.util.UUID;

@Builder(toBuilder = true)
Expand All @@ -27,6 +28,7 @@ public record ProviderApiKey(
@JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String createdBy,
@JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant lastUpdatedAt,
@JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String lastUpdatedBy){

@Override
public String toString() {
return "ProviderApiKey{" +
Expand All @@ -46,4 +48,19 @@ public static class Write {
public static class Public {
}
}

public record ProviderApiKeyPage(
@JsonView( {
Project.View.Public.class}) int page,
@JsonView({View.Public.class}) int size,
@JsonView({View.Public.class}) long total,
@JsonView({View.Public.class}) List<ProviderApiKey> content,
@JsonView({View.Public.class}) List<String> sortableBy)
implements
com.comet.opik.api.Page<ProviderApiKey>{

public static ProviderApiKeyPage empty(int page) {
return new ProviderApiKeyPage(page, 0, 0, List.of(), List.of());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.comet.opik.api.resources.v1.priv;

import com.codahale.metrics.annotation.Timed;
import com.comet.opik.api.Page;
import com.comet.opik.api.Project;
import com.comet.opik.api.ProviderApiKey;
import com.comet.opik.api.ProviderApiKeyUpdate;
import com.comet.opik.api.error.ErrorMessage;
Expand Down Expand Up @@ -46,21 +48,37 @@ public class LlmProviderApiKeyResource {
private final @NonNull LlmProviderApiKeyService llmProviderApiKeyService;
private final @NonNull Provider<RequestContext> requestContext;

@GET
@Operation(operationId = "findLlmProviderKeys", summary = "Find LLM Provider's ApiKeys", description = "Find LLM Provider's ApiKeys", responses = {
@ApiResponse(responseCode = "200", description = "LLMProviderApiKey resource", content = @Content(schema = @Schema(implementation = Project.ProjectPage.class)))
})
@JsonView({ProviderApiKey.View.Public.class})
public Response find() {

String workspaceId = requestContext.get().getWorkspaceId();

log.info("Find LLM Provider's ApiKeys for workspaceId '{}'", workspaceId);
Page<ProviderApiKey> providerApiKeyPage = llmProviderApiKeyService.find(workspaceId);
log.info("Found LLM Provider's ApiKeys for workspaceId '{}'", workspaceId);

return Response.ok().entity(providerApiKeyPage).build();
}

@GET
@Path("{id}")
@Operation(operationId = "getLlmProviderApiKeyById", summary = "Get LLM Provider's ApiKey by id", description = "Get LLM Provider's ApiKey by id", responses = {
@ApiResponse(responseCode = "200", description = "ProviderApiKey resource", content = @Content(schema = @Schema(implementation = ProviderApiKey.class))),
@ApiResponse(responseCode = "200", description = "LLMProviderApiKey resource", content = @Content(schema = @Schema(implementation = ProviderApiKey.class))),
@ApiResponse(responseCode = "404", description = "Not found", content = @Content(schema = @Schema(implementation = ErrorMessage.class)))})
@JsonView({ProviderApiKey.View.Public.class})
public Response getById(@PathParam("id") UUID id) {

String workspaceId = requestContext.get().getWorkspaceId();

log.info("Getting Provider's ApiKey by id '{}' on workspace_id '{}'", id, workspaceId);
log.info("Getting LLM Provider's ApiKey by id '{}' on workspace_id '{}'", id, workspaceId);

ProviderApiKey providerApiKey = llmProviderApiKeyService.get(id, workspaceId);
ProviderApiKey providerApiKey = llmProviderApiKeyService.find(id, workspaceId);

log.info("Got Provider's ApiKey by id '{}' on workspace_id '{}'", id, workspaceId);
log.info("Got LLM Provider's ApiKey by id '{}' on workspace_id '{}'", id, workspaceId);

return Response.ok().entity(providerApiKey).build();
}
Expand All @@ -77,9 +95,9 @@ public Response saveApiKey(
@Context UriInfo uriInfo) {
String workspaceId = requestContext.get().getWorkspaceId();
String userName = requestContext.get().getUserName();
log.info("Save api key for provider '{}', on workspace_id '{}'", providerApiKey.provider(), workspaceId);
log.info("Save api key for LLM provider '{}', on workspace_id '{}'", providerApiKey.provider(), workspaceId);
var providerApiKeyId = llmProviderApiKeyService.saveApiKey(providerApiKey, userName, workspaceId).id();
log.info("Saved api key for provider '{}', on workspace_id '{}'", providerApiKey.provider(), workspaceId);
log.info("Saved api key for LLM provider '{}', on workspace_id '{}'", providerApiKey.provider(), workspaceId);

var uri = uriInfo.getAbsolutePathBuilder().path("/%s".formatted(providerApiKeyId)).build();

Expand All @@ -99,9 +117,9 @@ public Response updateApiKey(@PathParam("id") UUID id,
String workspaceId = requestContext.get().getWorkspaceId();
String userName = requestContext.get().getUserName();

log.info("Updating api key for provider with id '{}' on workspaceId '{}'", id, workspaceId);
log.info("Updating api key for LLM provider with id '{}' on workspaceId '{}'", id, workspaceId);
llmProviderApiKeyService.updateApiKey(id, providerApiKeyUpdate, userName, workspaceId);
log.info("Updated api key for provider with id '{}' on workspaceId '{}'", id, workspaceId);
log.info("Updated api key for LLM provider with id '{}' on workspaceId '{}'", id, workspaceId);

return Response.noContent().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

Expand All @@ -32,6 +33,10 @@ void update(@Bind("id") UUID id,
@SqlQuery("SELECT * FROM llm_provider_api_key WHERE id = :id AND workspace_id = :workspaceId")
ProviderApiKey findById(@Bind("id") UUID id, @Bind("workspaceId") String workspaceId);

@SqlQuery("SELECT * FROM llm_provider_api_key " +
" WHERE workspace_id = :workspaceId ")
List<ProviderApiKey> find(@Bind("workspaceId") String workspaceId);

default Optional<ProviderApiKey> fetch(UUID id, String workspaceId) {
return Optional.ofNullable(findById(id, workspaceId));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.comet.opik.domain;

import com.comet.opik.api.Page;
import com.comet.opik.api.ProviderApiKey;
import com.comet.opik.api.ProviderApiKeyUpdate;
import com.comet.opik.api.error.EntityAlreadyExistsException;
Expand All @@ -12,6 +13,7 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate;

Expand All @@ -25,7 +27,8 @@
@ImplementedBy(LlmProviderApiKeyServiceImpl.class)
public interface LlmProviderApiKeyService {

ProviderApiKey get(UUID id, String workspaceId);
ProviderApiKey find(UUID id, String workspaceId);
Page<ProviderApiKey> find(String workspaceId);
ProviderApiKey saveApiKey(ProviderApiKey providerApiKey, String userName, String workspaceId);
void updateApiKey(UUID id, ProviderApiKeyUpdate providerApiKeyUpdate, String userName, String workspaceId);
}
Expand All @@ -40,23 +43,37 @@ class LlmProviderApiKeyServiceImpl implements LlmProviderApiKeyService {
private final @NonNull TransactionTemplate template;

@Override
public ProviderApiKey get(UUID id, String workspaceId) {
log.info("Getting provider api key with id '{}', workspaceId '{}'", id, workspaceId);
public ProviderApiKey find(@NonNull UUID id, @NonNull String workspaceId) {

ProviderApiKey providerApiKey = template.inTransaction(READ_ONLY, handle -> {

var repository = handle.attach(LlmProviderApiKeyDAO.class);

return repository.fetch(id, workspaceId).orElseThrow(this::createNotFoundError);
});
log.info("Got provider api key with id '{}', workspaceId '{}'", id, workspaceId);

return providerApiKey.toBuilder()
.build();
}

@Override
public ProviderApiKey saveApiKey(@NonNull ProviderApiKey providerApiKey, String userName, String workspaceId) {
public Page<ProviderApiKey> find(@NonNull String workspaceId) {
List<ProviderApiKey> providerApiKeys = template.inTransaction(READ_ONLY, handle -> {
var repository = handle.attach(LlmProviderApiKeyDAO.class);
return repository.find(workspaceId);
});

if (CollectionUtils.isEmpty(providerApiKeys)) {
return ProviderApiKey.ProviderApiKeyPage.empty(0);
}

return new ProviderApiKey.ProviderApiKeyPage(
0, providerApiKeys.size(), providerApiKeys.size(),
providerApiKeys, List.of());
}

@Override
public ProviderApiKey saveApiKey(@NonNull ProviderApiKey providerApiKey, @NonNull String userName, @NonNull String workspaceId) {
UUID apiKeyId = idGenerator.generateId();

var newProviderApiKey = providerApiKey.toBuilder()
Expand All @@ -74,7 +91,7 @@ public ProviderApiKey saveApiKey(@NonNull ProviderApiKey providerApiKey, String
return newProviderApiKey;
});

return get(apiKeyId, workspaceId);
return find(apiKeyId, workspaceId);
} catch (UnableToExecuteStatementException e) {
if (e.getCause() instanceof SQLIntegrityConstraintViolationException) {
throw newConflict();
Expand All @@ -85,8 +102,8 @@ public ProviderApiKey saveApiKey(@NonNull ProviderApiKey providerApiKey, String
}

@Override
public void updateApiKey(@NonNull UUID id, @NonNull ProviderApiKeyUpdate providerApiKeyUpdate, String userName,
String workspaceId) {
public void updateApiKey(@NonNull UUID id, @NonNull ProviderApiKeyUpdate providerApiKeyUpdate, @NonNull String userName,
@NonNull String workspaceId) {

template.inTransaction(WRITE, handle -> {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.comet.opik.infrastructure;

import jakarta.inject.Inject;
import lombok.NonNull;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
Expand All @@ -16,29 +16,17 @@

public class EncryptionUtils {

private static void init() {
if (key != null) return;
synchronized (EncryptionUtils.class) {
if (key == null) {
byte[] keyBytes = config.getEncryption().getKey().getBytes(StandardCharsets.UTF_8);
key = new SecretKeySpec(keyBytes, ALGO);
}
}
}

private static final String ALGO = "AES";
private static final Base64.Encoder mimeEncoder = Base64.getMimeEncoder();
private static final Base64.Decoder mimeDecoder = Base64.getMimeDecoder();
private static OpikConfiguration config;
private static Key key;

@Inject
static void setConfig(OpikConfiguration config) {
EncryptionUtils.config = config;
public static void setConfig(@NonNull OpikConfiguration config) {
byte[] keyBytes = config.getEncryption().getKey().getBytes(StandardCharsets.UTF_8);
key = new SecretKeySpec(keyBytes, ALGO);
}

public static String encrypt(String data) {
init();
public static String encrypt(@NonNull String data) {
try {
Cipher c = Cipher.getInstance(ALGO);
c.init(Cipher.ENCRYPT_MODE, key);
Expand All @@ -50,8 +38,7 @@ public static String encrypt(String data) {
}
}

public static String decrypt(String encryptedData) {
init();
public static String decrypt(@NonNull String encryptedData) {
try {
Cipher c = Cipher.getInstance(ALGO);
c.init(Cipher.DECRYPT_MODE, key);
Expand Down

This file was deleted.

Loading

0 comments on commit 363b14f

Please sign in to comment.