Files
ResolveSpec/pkg/resolvemcp/writecols.go
T
Hein ad2f54693f feat(resolvemcp): require authentication on MCP endpoints and enforce model rules on writes
Guard() rejects unauthenticated callers (no guest/optional mode); Setup*/New* helpers take a
SecurityList and have explicit *Unauthenticated variants. Model rules now reach the security
hooks, create checks CanCreate (security.CheckModelCreateAllowed), create/update validate keys
against the model's writable columns, update sets only given keys (NULL allowed), update and
delete go through row security via a new BeforeScan hook, and the annotation tool is opt-in
(Config.EnableAnnotations) and runs BeforeHandle.
2026-10-01 13:31:13 +02:00

57 lines
1.8 KiB
Go

package resolvemcp
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/bitechdev/ResolveSpec/pkg/reflection"
)
// maxKeyEcho caps how much of a rejected client key is echoed back in an error.
const maxKeyEcho = 64
// writeColumns validates the keys of a create/update payload against the model and returns the
// values keyed by database column name. A key may be the json name or the column name of a
// writable field (case-insensitive); relations, scan-only and unexported fields are not
// writable. Unknown keys are rejected rather than dropped, so a client learns that a write
// did not take effect, and no client-chosen identifier reaches SQL.
func writeColumns(model interface{}, data map[string]interface{}) (map[string]interface{}, error) {
modelType := reflect.TypeOf(model)
for modelType != nil && (modelType.Kind() == reflect.Pointer || modelType.Kind() == reflect.Slice) {
modelType = modelType.Elem()
}
if modelType == nil || modelType.Kind() != reflect.Struct {
return nil, fmt.Errorf("invalid model")
}
accepted := make(map[string]string)
for jsonKey, col := range reflection.BuildJSONToDBColumnMap(modelType) {
accepted[strings.ToLower(jsonKey)] = col
accepted[strings.ToLower(col)] = col
}
out := make(map[string]interface{}, len(data))
var unknown []string
for key, value := range data {
col, ok := accepted[strings.ToLower(key)]
if !ok {
if len(key) > maxKeyEcho {
key = key[:maxKeyEcho] + "..."
}
unknown = append(unknown, key)
continue
}
if _, dup := out[col]; dup {
return nil, fmt.Errorf("column %q given more than once", col)
}
out[col] = value
}
if len(unknown) > 0 {
sort.Strings(unknown)
return nil, fmt.Errorf("unknown or read-only fields: %s", strings.Join(unknown, ", "))
}
return out, nil
}