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.
106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-golem-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
// PermissionAccessSettings guards the AI models screen.
|
|
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, unless the same update changes the adapter or base URL.
|
|
type ModelsAdminController struct{}
|
|
|
|
var (
|
|
_ pact.AdminController = ModelsAdminController{}
|
|
_ pact.AdminPermissioned = ModelsAdminController{}
|
|
_ pact.AdminRecordSource = ModelsAdminController{}
|
|
_ pact.FormBeforeCreate = ModelsAdminController{}
|
|
_ pact.FormBeforeUpdate = ModelsAdminController{}
|
|
)
|
|
|
|
// ID is golem15.golem.models.
|
|
func (ModelsAdminController) ID() string { return "golem15.golem.models" }
|
|
|
|
// ModelName is the model's WinterCMS class name.
|
|
func (ModelsAdminController) ModelName() string { return `Golem15\Golem\Models\AiModel` }
|
|
|
|
// ConfigDir holds config_list.yaml and config_form.yaml.
|
|
func (ModelsAdminController) ConfigDir() string { return "controllers/aimodels" }
|
|
|
|
// RequiredPermissions is golem15.golem.access_settings.
|
|
func (ModelsAdminController) RequiredPermissions() []string {
|
|
return []string{PermissionAccessSettings}
|
|
}
|
|
|
|
// NewRecord returns an empty AI model.
|
|
func (ModelsAdminController) NewRecord() any { return &models.AiModel{} }
|
|
|
|
// redacted is how lagoon.Encrypted serializes, so how the form shows the
|
|
// stored key.
|
|
const redacted = "[redacted]"
|
|
|
|
func keyMissing(m *models.AiModel) bool {
|
|
k := m.APIKey.Reveal()
|
|
return k == "" || k == redacted
|
|
}
|
|
|
|
// FormBeforeCreate requires an API key.
|
|
func (ModelsAdminController) FormBeforeCreate(_ context.Context, model any) error {
|
|
m, ok := model.(*models.AiModel)
|
|
if !ok {
|
|
return errors.New("golem: unexpected admin record")
|
|
}
|
|
if keyMissing(m) {
|
|
return &cabana.ValidationError{Details: map[string]any{"api_key": []string{"The api key field is required."}}}
|
|
}
|
|
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. 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 {
|
|
return errors.New("golem: unexpected admin record")
|
|
}
|
|
if !keyMissing(m) {
|
|
return nil
|
|
}
|
|
tx, ok := cabana.TxFromContext(ctx)
|
|
if !ok {
|
|
return errors.New("golem: no transaction for the model update")
|
|
}
|
|
var stored models.AiModel
|
|
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."}}}
|
|
}
|
|
return nil
|
|
}
|