Removes standalone "// v0.X.X:" comment lines and inline trailing version annotations. Keeps version references in config field docs and compatibility notes where they describe requirements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
148 lines
3.9 KiB
Go
148 lines
3.9 KiB
Go
package store
|
|
|
|
// settings_cascade.go
|
|
//
|
|
// Pure functions for three-tier settings resolution:
|
|
// global (admin) → team → user
|
|
//
|
|
// Two orthogonal axes:
|
|
// - RBAC controls who can write at each scope
|
|
// - user_overridable (per setting key) controls whether lower scopes
|
|
// can override higher scope values
|
|
//
|
|
// The kernel enforces the cascade without knowing setting semantics.
|
|
|
|
import "encoding/json"
|
|
|
|
// SettingSchema represents one entry from the manifest "settings" array.
|
|
type SettingSchema struct {
|
|
Key string `json:"key"`
|
|
UserOverridable *bool `json:"user_overridable,omitempty"`
|
|
}
|
|
|
|
// IsUserOverridable returns whether a setting key can be overridden at
|
|
// lower scopes (team, user). Defaults to true when the field is absent
|
|
// for backward compatibility with existing manifests.
|
|
func (s SettingSchema) IsUserOverridable() bool {
|
|
return s.UserOverridable == nil || *s.UserOverridable
|
|
}
|
|
|
|
// ParseSettingsSchema extracts []SettingSchema from a package manifest.
|
|
func ParseSettingsSchema(manifest map[string]any) []SettingSchema {
|
|
arr, ok := manifest["settings"].([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
result := make([]SettingSchema, 0, len(arr))
|
|
for _, item := range arr {
|
|
m, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
s := SettingSchema{}
|
|
s.Key, _ = m["key"].(string)
|
|
if s.Key == "" {
|
|
continue
|
|
}
|
|
if uo, ok := m["user_overridable"].(bool); ok {
|
|
s.UserOverridable = &uo
|
|
}
|
|
result = append(result, s)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ResolveSettings implements the settings cascade:
|
|
//
|
|
// user override → team override → global default
|
|
//
|
|
// For each key declared in the schema with user_overridable: false,
|
|
// team and user values are ignored — only the global value is used.
|
|
// Keys not declared in schema default to overridable.
|
|
//
|
|
// Parameters:
|
|
// - globalSettings: admin-set values from packages.package_settings
|
|
// - teamSettings: team-scoped overrides (nil if no team context)
|
|
// - userSettings: per-user overrides (nil if no user context)
|
|
// - schema: parsed manifest settings array
|
|
func ResolveSettings(
|
|
globalSettings json.RawMessage,
|
|
teamSettings json.RawMessage,
|
|
userSettings json.RawMessage,
|
|
schema []SettingSchema,
|
|
) map[string]any {
|
|
global := jsonToMap(globalSettings)
|
|
team := jsonToMap(teamSettings)
|
|
user := jsonToMap(userSettings)
|
|
|
|
// Build overridable lookup from schema
|
|
overridable := make(map[string]bool, len(schema))
|
|
for _, s := range schema {
|
|
overridable[s.Key] = s.IsUserOverridable()
|
|
}
|
|
|
|
// Start with global as base
|
|
merged := make(map[string]any, len(global))
|
|
for k, v := range global {
|
|
merged[k] = v
|
|
}
|
|
|
|
// Overlay team values (respecting user_overridable)
|
|
for k, v := range team {
|
|
canOverride, declared := overridable[k]
|
|
if !declared {
|
|
canOverride = true // unknown keys default to overridable
|
|
}
|
|
if canOverride {
|
|
merged[k] = v
|
|
}
|
|
}
|
|
|
|
// Overlay user values (respecting user_overridable)
|
|
for k, v := range user {
|
|
canOverride, declared := overridable[k]
|
|
if !declared {
|
|
canOverride = true
|
|
}
|
|
if canOverride {
|
|
merged[k] = v
|
|
}
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
// FilterOverridableKeys strips non-overridable keys from a settings map.
|
|
// Used by handlers to enforce user_overridable before saving.
|
|
// Returns the filtered map and the list of rejected keys.
|
|
func FilterOverridableKeys(settings map[string]any, schema []SettingSchema) (map[string]any, []string) {
|
|
overridable := make(map[string]bool, len(schema))
|
|
for _, s := range schema {
|
|
overridable[s.Key] = s.IsUserOverridable()
|
|
}
|
|
|
|
filtered := make(map[string]any, len(settings))
|
|
var rejected []string
|
|
for k, v := range settings {
|
|
canOverride, declared := overridable[k]
|
|
if !declared {
|
|
canOverride = true
|
|
}
|
|
if canOverride {
|
|
filtered[k] = v
|
|
} else {
|
|
rejected = append(rejected, k)
|
|
}
|
|
}
|
|
return filtered, rejected
|
|
}
|
|
|
|
func jsonToMap(raw json.RawMessage) map[string]any {
|
|
if len(raw) == 0 {
|
|
return nil
|
|
}
|
|
var m map[string]any
|
|
json.Unmarshal(raw, &m)
|
|
return m
|
|
}
|