This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/settings_cascade.go
Jeffrey Smith 3d4228f868
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / test-go-pg (push) Successful in 2m47s
CI/CD / build-and-deploy (push) Successful in 26s
Feat v0.6.3 dead code sweep (#38)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 12:37:47 +00:00

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
}