fix(14): WR-04 require the AI key again when the endpoint changes

An admin who cannot read the write-only key could point base_url at
their own host, leave the key redacted and have it sent there on the
next call. An update that keeps the stored key may no longer change the
adapter or base URL; changing either needs the key re-entered.
This commit is contained in:
Jakub Zych
2026-10-04 08:08:07 +02:00
parent 6646d10ff5
commit 93b2b05476
3 changed files with 46 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ package controllers
import (
"context"
"errors"
"strings"
"git.golem15.com/golem15/sm-golem-plugin/models"
"git.golem15.com/golem15/summercms/modules/cabana"
@@ -15,7 +16,7 @@ const PermissionAccessSettings = "golem15.golem.access_settings"
// ModelsAdminController is the AI models list and form
// (golem15.golem.models). The API key is write-only: the form shows it
// redacted, and an update that sends it back empty or redacted keeps the
// stored key.
// stored key, unless the same update changes the adapter or base URL.
type ModelsAdminController struct{}
var (
@@ -64,8 +65,19 @@ func (ModelsAdminController) FormBeforeCreate(_ context.Context, model any) erro
return nil
}
// baseURLOf is a model's base URL with nil and blank alike.
func baseURLOf(m *models.AiModel) string {
if m.BaseURL == nil {
return ""
}
return strings.TrimSpace(*m.BaseURL)
}
// FormBeforeUpdate keeps the stored key when the form sent it back empty
// or redacted.
// or redacted. A kept key never follows a changed endpoint: when the
// adapter or base URL differs from the stored one, the key must be entered
// again, so an operator who cannot read the key cannot redirect it to a
// host they control either.
func (ModelsAdminController) FormBeforeUpdate(ctx context.Context, model any) error {
m, ok := model.(*models.AiModel)
if !ok {
@@ -79,9 +91,12 @@ func (ModelsAdminController) FormBeforeUpdate(ctx context.Context, model any) er
return errors.New("golem: no transaction for the model update")
}
var stored models.AiModel
if err := tx.Select("api_key").Where("id = ?", m.ID).Take(&stored).Error; err != nil {
if err := tx.Select("api_key", "adapter", "base_url").Where("id = ?", m.ID).Take(&stored).Error; err != nil {
return err
}
if stored.Adapter != m.Adapter || baseURLOf(&stored) != baseURLOf(m) {
return &cabana.ValidationError{Details: map[string]any{"api_key": []string{"Re-enter the API key when changing the adapter or base URL."}}}
}
m.APIKey = stored.APIKey
if keyMissing(m) {
return &cabana.ValidationError{Details: map[string]any{"api_key": []string{"The api key field is required."}}}