Skip to content

Commit d2dd1e4

Browse files
committed
fix(llm-auth): own the api_key/auth_config exclusion in one method, document the clearing
1 parent 748f331 commit d2dd1e4

2 files changed

Lines changed: 20 additions & 11 deletions

File tree

apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/LlmProviderApiKeyResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public Response saveApiKey(
135135
@PATCH
136136
@Path("{id}")
137137
@RequiredPermissions(WorkspaceUserPermission.AI_PROVIDER_UPDATE)
138-
@Operation(operationId = "updateLlmProviderApiKey", summary = "Update LLM Provider's ApiKey", description = "Update LLM Provider's ApiKey", responses = {
138+
@Operation(operationId = "updateLlmProviderApiKey", summary = "Update LLM Provider's ApiKey", description = "Update LLM Provider's ApiKey. api_key and auth_config are mutually exclusive: setting a valid auth_config on a provider that holds a static api_key clears the stored key; send auth_config as an empty object to clear the recipe and switch back to a static key", responses = {
139139
@ApiResponse(responseCode = "204", description = "No Content"),
140140
@ApiResponse(responseCode = "401", description = "Bad Request", content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
141141
@ApiResponse(responseCode = "403", description = "Access forbidden", content = @Content(schema = @Schema(implementation = ErrorMessage.class))),

apps/opik-backend/src/main/java/com/comet/opik/domain/LlmProviderApiKeyService.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,10 @@ public void updateApiKey(@NonNull UUID id, @NonNull ProviderApiKeyUpdate provide
176176

177177
var authConfigUpdate = resolveAuthConfigUpdate(providerApiKeyUpdate, providerApiKey);
178178

179-
var update = providerApiKeyUpdate;
180-
if (authConfigUpdate.authConfig() != null && update.apiKey() == null) {
181-
update = update.toBuilder().apiKey(EncryptionUtils.encrypt("")).build();
182-
}
183-
184179
repository.update(providerApiKey.id(),
185180
workspaceId,
186181
userName,
187-
update,
182+
authConfigUpdate.effectiveUpdate(),
188183
authConfigUpdate.clear(),
189184
authConfigUpdate.authConfig());
190185

@@ -228,7 +223,15 @@ private ProviderAuthConfig resolveAuthConfigForTest(ProviderAuthCheck request, S
228223
return mergeSecretSentinels(incoming, stored);
229224
}
230225

231-
private record AuthConfigUpdate(boolean clear, ProviderAuthConfig authConfig) {
226+
/**
227+
* The resolved auth_config decision plus the effective update to persist. The api_key /
228+
* auth_config mutual exclusion is enforced entirely inside
229+
* {@link #resolveAuthConfigUpdate}: it both validates the incoming pair and blanks the
230+
* stored key when the update switches to token auth, so callers persist
231+
* {@code effectiveUpdate} as-is and cannot hold the invariant wrong.
232+
*/
233+
private record AuthConfigUpdate(ProviderApiKeyUpdate effectiveUpdate, boolean clear,
234+
ProviderAuthConfig authConfig) {
232235
}
233236

234237
/**
@@ -241,10 +244,10 @@ private AuthConfigUpdate resolveAuthConfigUpdate(ProviderApiKeyUpdate update, Pr
241244
if (incoming == null) {
242245
validateNoStaticKeyConflict(
243246
update.apiKey() != null ? update.apiKey() : stored.apiKey(), stored.authConfig());
244-
return new AuthConfigUpdate(false, null);
247+
return new AuthConfigUpdate(update, false, null);
245248
}
246249
if (incoming.isEmpty()) {
247-
return new AuthConfigUpdate(true, null);
250+
return new AuthConfigUpdate(update, true, null);
248251
}
249252
if (!stored.provider().supportsDynamicTokenAuth()) {
250253
throw new BadRequestException(
@@ -254,9 +257,15 @@ private AuthConfigUpdate resolveAuthConfigUpdate(ProviderApiKeyUpdate update, Pr
254257
if (!errors.isEmpty()) {
255258
throw new BadRequestException(String.join("; ", errors));
256259
}
260+
// only an api_key set in this same request conflicts: an update carrying none switches
261+
// the provider to token auth, which implicitly clears the stored static key below —
262+
// the dialog hides the key field in token mode, so the swap must be self-contained
257263
validateNoStaticKeyConflict(update.apiKey(), incoming);
258264
var merged = mergeSecretSentinels(incoming, stored.authConfig());
259-
return new AuthConfigUpdate(false, merged);
265+
var effectiveUpdate = update.apiKey() == null
266+
? update.toBuilder().apiKey(EncryptionUtils.encrypt("")).build()
267+
: update;
268+
return new AuthConfigUpdate(effectiveUpdate, false, merged);
260269
}
261270

262271
private void validateNoStaticKeyConflict(String encryptedApiKey, ProviderAuthConfig authConfig) {

0 commit comments

Comments
 (0)