Skip to content

Admin API: no way to erase a write-only secret on entity update (PUT) #1949

Description

@KirylKurnosenka

Name and Version

AI DIAL Core

What steps will reproduce the bug?

  1. Create a model with a secret extra data on the upstream:

    PUT /v1/models/{bucket}/my-model
    { "upstreams": [ { "endpoint": "https://vendor.example/v1/chat/completions", "secretExtraData": "{\"field\":\"value\"}" } ], ... }
    

    → 200 OK

  2. Later, switch the upstream to a plain-string extraData (intent: replace the pair):

    PUT /v1/models/{bucket}/my-model
    { "upstreams": [ { "endpoint": "https://vendor.example/v1/chat/completions", "extraData": "abc" } ], ... }
    

    422 When both extraData and secretExtraData are set, both must be JSON objects

    The omitted secretExtraData is silently preserved from the stored entity (omit-preserve in SecretFieldProcessor.mergeInto: current == null || current.isNull() → copy prior ciphertext), so after the merge both fields are set and UpstreamExtraDataMerger.validateNoOverlap (via ConfigResourceController.checkCrossReferences) rejects the non-JSON extraData.

  3. Try "secretExtraData": null instead — pre-fix it hits the same preserve branch, so the outcome is identical. No request clears the field; the only workaround is deleting and recreating the model.

What is the expected behavior?

A client must be able to distinguish three update intents for a secret field:

  • omitted → keep the stored secret
  • explicit null → erase the secret
  • a value → replace the secret

With null meaning erase, step 2 sent as { "extraData": "abc", "secretExtraData": null } should return 200 and leave the upstream with plain extraData: "abc" and no secret extra data.

What do you see instead?

An explicit null is treated identically to an omitted field — the stored secret is silently preserved. There is no request that can remove a secret: GET drops write-only (@EncryptedField) fields entirely, so a client cannot read the value to unset it any other way. Once a JSON secretExtraData is stored, the upstream can never be switched to a plain-string extraData without deleting the entity.

The same applies to Upstream.key: after "key": null the credential is still sent as X-UPSTREAM-KEY on proxied requests. Affected fields: Upstream.key, Upstream.secretExtraData, UpstreamInterface.key, UpstreamInterface.secretExtraData (via Model.upstreams, Route.upstreams, Upstream.interfaces) on per-entity PUT. Key.key is mandatory on PUT (400 on blank/null), so erase does not apply there. An empty string "" incidentally overwrites the secret with "" — undocumented and unpinned by tests.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

  • Status
    Code Review

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions