diff --git a/data/defaults.yml b/data/defaults.yml index 08681bc5..b03b8096 100644 --- a/data/defaults.yml +++ b/data/defaults.yml @@ -32,11 +32,11 @@ settings: access: endpoints: - - !/v1/about - - !/v1/configuration - - !/v1/devices - - !/v1/register - - !/v1/unregister - - !/v1/qrcodelink - - !/v1/accounts - - !/v1/contacts + - "!/v1/about" + - "!/v1/configuration" + - "!/v1/devices" + - "!/v1/register" + - "!/v1/unregister" + - "!/v1/qrcodelink" + - "!/v1/accounts" + - "!/v1/contacts" diff --git a/internals/config/loader.go b/internals/config/loader.go index b4ca9c8d..fee3b818 100644 --- a/internals/config/loader.go +++ b/internals/config/loader.go @@ -25,13 +25,15 @@ var ENV *structure.ENV = &structure.ENV{ INSECURE: false, } -var defaultsConf = configutils.New() -var userConf = configutils.New() -var tokenConf = configutils.New() +var defaultsConf *configutils.Config +var userConf *configutils.Config +var tokenConf *configutils.Config -var config = configutils.New() +var mainConf *configutils.Config func Load() { + Clear() + InitReload() LoadDefaults() @@ -42,10 +44,14 @@ func Load() { userConf.LoadEnv() - config.MergeLayers(defaultsConf.Layer, userConf.Layer) + NormalizeConfig(defaultsConf) + NormalizeConfig(userConf) + + NormalizeTokens() + + mainConf.MergeLayers(defaultsConf.Layer, userConf.Layer) - config.NormalizeKeys() - config.TemplateConfig() + mainConf.TemplateConfig() InitTokens() @@ -53,10 +59,59 @@ func Load() { log.Info("Finished Loading Configuration") - log.Dev("Loaded Config:\n" + jsonutils.ToJson(config.Layer.All())) + log.Dev("Loaded Config:\n" + jsonutils.ToJson(mainConf.Layer.All())) log.Dev("Loaded Token Configs:\n" + jsonutils.ToJson(tokenConf.Layer.All())) } +func Clear() { + defaultsConf = configutils.New() + userConf = configutils.New() + tokenConf = configutils.New() + mainConf = configutils.New() +} + +func LowercaseKeys(config *configutils.Config) { + data := map[string]any{} + + for _, key := range config.Layer.Keys() { + lower := strings.ToLower(key) + + data[lower] = config.Layer.Get(key) + } + + config.Layer.Delete("") + config.Load(data, "") +} + +func NormalizeConfig(config *configutils.Config) { + Normalize(config, "settings", &structure.SETTINGS{}) +} + +func Normalize(config *configutils.Config, path string, structure any) { + data := config.Layer.Get(path) + old, ok := data.(map[string]any) + + if !ok { + log.Warn("Could not load `"+path+"`") + return + } + + // Create temporary config + tmpConf := configutils.New() + tmpConf.Load(old, "") + + // Apply transforms to the new config + tmpConf.ApplyTransformFuncs(structure, "", transformFuncs) + + // Lowercase actual config + LowercaseKeys(config) + + // Load temporary config back into paths + config.Layer.Delete(path) + + config.Load(tmpConf.Layer.All(), path) +} + func InitReload() { defaultsConf.OnLoad(Load) userConf.OnLoad(Load) @@ -64,17 +119,15 @@ func InitReload() { } func InitEnv() { - ENV.PORT = strconv.Itoa(config.Layer.Int("service.port")) + ENV.PORT = strconv.Itoa(mainConf.Layer.Int("service.port")) - ENV.LOG_LEVEL = strings.ToLower(config.Layer.String("loglevel")) + ENV.LOG_LEVEL = strings.ToLower(mainConf.Layer.String("loglevel")) - ENV.API_URL = config.Layer.String("api.url") + ENV.API_URL = mainConf.Layer.String("api.url") var settings structure.SETTINGS - config.TransformChildren("settings.message.variables", transformVariables) - - config.Layer.Unmarshal("settings", &settings) + mainConf.Layer.Unmarshal("settings", &settings) ENV.SETTINGS["*"] = &settings } @@ -101,8 +154,4 @@ func LoadConfig() { log.Error("Could not Load Config ", ENV.CONFIG_PATH, ": ", err.Error()) } -} - -func transformVariables(key string, value any) (string, any) { - return strings.ToUpper(key), value -} +} \ No newline at end of file diff --git a/internals/config/parser.go b/internals/config/parser.go new file mode 100644 index 00000000..96acd486 --- /dev/null +++ b/internals/config/parser.go @@ -0,0 +1,23 @@ +package config + +import ( + "strings" +) + +var transformFuncs = map[string]func(string, any) (string, any) { + "default": defaultTransform, + "lower": lowercaseTransform, + "upper": uppercaseTransform, +} + +func defaultTransform(key string, value any) (string, any) { + return key, value +} + +func lowercaseTransform(key string, value any) (string, any) { + return strings.ToLower(key), value +} + +func uppercaseTransform(key string, value any) (string, any) { + return strings.ToUpper(key), value +} \ No newline at end of file diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 0fd2e428..d9f2120e 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -9,32 +9,32 @@ type ENV struct { PORT string API_URL string API_TOKENS []string - SETTINGS map[string]*SETTINGS + SETTINGS map[string]*SETTINGS `koanf:"settings"` INSECURE bool } type SETTINGS struct { - ACCESS ACCESS_SETTINGS `koanf:"access"` - MESSAGE MESSAGE_SETTINGS `koanf:"message"` + ACCESS ACCESS_SETTINGS `koanf:"access" transform:"lower"` + MESSAGE MESSAGE_SETTINGS `koanf:"message" transform:"lower"` } type MESSAGE_SETTINGS struct { - VARIABLES map[string]any `koanf:"variables"` - FIELD_MAPPINGS map[string][]FieldMapping `koanf:"fieldmappings"` - TEMPLATE string `koanf:"template"` + VARIABLES map[string]any `koanf:"variables" childtransform:"upper"` + FIELD_MAPPINGS map[string][]FieldMapping `koanf:"fieldmappings" childtransform:"default"` + TEMPLATE string `koanf:"template" transform:"lower"` } type FieldMapping struct { - Field string `koanf:"field"` - Score int `koanf:"score"` + Field string `koanf:"field" transform:"lower"` + Score int `koanf:"score" transform:"lower"` } type ACCESS_SETTINGS struct { - ENDPOINTS []string `koanf:"endpoints"` - FIELD_POLOCIES map[string]FieldPolicy `koanf:"fieldpolicies"` + ENDPOINTS []string `koanf:"endpoints" transform:"lower"` + FIELD_POLICIES map[string]FieldPolicy `koanf:"fieldpolicies" transform:"lower" childtransform:"default"` } type FieldPolicy struct { - Value any `koanf:"value"` - Action string `koanf:"action"` + Value any `koanf:"value" transform:"lower"` + Action string `koanf:"action" transform:"lower"` } \ No newline at end of file diff --git a/internals/config/tokens.go b/internals/config/tokens.go index d963312f..69ec8518 100644 --- a/internals/config/tokens.go +++ b/internals/config/tokens.go @@ -4,11 +4,12 @@ import ( "strconv" "github.com/codeshelldev/secured-signal-api/internals/config/structure" + "github.com/codeshelldev/secured-signal-api/utils/configutils" log "github.com/codeshelldev/secured-signal-api/utils/logger" "github.com/knadh/koanf/parsers/yaml" ) -type TOKEN_CONFIG_ struct { +type TOKEN_CONFIG struct { TOKENS []string `koanf:"tokens"` OVERRIDES structure.SETTINGS `koanf:"overrides"` } @@ -22,17 +23,29 @@ func LoadTokens() { log.Error("Could not Load Configs in ", ENV.TOKENS_DIR, ": ", err.Error()) } - tokenConf.NormalizeKeys() - tokenConf.TemplateConfig() } -func InitTokens() { - apiTokens := config.Layer.Strings("api.tokens") +func NormalizeTokens() { + configArray := []map[string]any{} + + for _, config := range tokenConf.Layer.Slices("tokenconfigs") { + tmpConf := configutils.New() + tmpConf.Load(config.All(), "") + + Normalize(tmpConf, "overrides", &structure.SETTINGS{}) + + configArray = append(configArray, tmpConf.Layer.All()) + } + + // Merge token configs together into new temporary config + tokenConf.Layer.Set("tokenconfigs", configArray) +} - var tokenConfigs []TOKEN_CONFIG_ +func InitTokens() { + apiTokens := mainConf.Layer.Strings("api.tokens") - tokenConf.TransformChildrenUnderArray("tokenconfigs", "overrides.message.variables", transformVariables) + var tokenConfigs []TOKEN_CONFIG tokenConf.Layer.Unmarshal("tokenconfigs", &tokenConfigs) @@ -53,7 +66,7 @@ func InitTokens() { // Set Blocked Endpoints on Config to User Layer Value // => effectively ignoring Default Layer - config.Layer.Set("settings.access.endpoints", userConf.Layer.Strings("settings.access.endpoints")) + mainConf.Layer.Set("settings.access.endpoints", userConf.Layer.Strings("settings.access.endpoints")) } if len(apiTokens) > 0 { @@ -63,7 +76,7 @@ func InitTokens() { } } -func parseTokenConfigs(configs []TOKEN_CONFIG_) map[string]structure.SETTINGS { +func parseTokenConfigs(configs []TOKEN_CONFIG) map[string]structure.SETTINGS { settings := map[string]structure.SETTINGS{} for _, config := range configs { diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index e7cfc6d9..2eb54c96 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -19,10 +19,10 @@ func policyHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { settings := getSettingsByReq(req) - policies := settings.ACCESS.FIELD_POLOCIES + policies := settings.ACCESS.FIELD_POLICIES if policies == nil { - policies = getSettings("*").ACCESS.FIELD_POLOCIES + policies = getSettings("*").ACCESS.FIELD_POLICIES } body, err := request.GetReqBody(req) diff --git a/utils/configutils/configutils.go b/utils/configutils/configutils.go index df5b0b62..b0686aa8 100644 --- a/utils/configutils/configutils.go +++ b/utils/configutils/configutils.go @@ -26,9 +26,7 @@ type Config struct { func New() *Config { return &Config{ Layer: koanf.New("."), - LoadFunc: func() { - log.Dev("Config.LoadFunc not initialized!") - }, + LoadFunc: func() {}, } } @@ -63,10 +61,64 @@ func WatchFile(path string, f *file.File, loadFunc func()) { configLock.Lock() defer configLock.Unlock() + f.Unwatch() + loadFunc() }) } +func getPath(str string) string { + if str == "." { + str = "" + } + + return str +} + +func (config *Config) Load(data map[string]any, path string) error { + parts := strings.Split(path, ".") + + res := map[string]any{} + + if path == "" { + res = data + } else { + for i, key := range parts { + if i == 0 { + res[key] = data + } else { + sub := map[string]any{} + + sub[key] = res + + res = sub + } + } + } + + return config.Layer.Load(confmap.Provider(res, "."), nil) +} + +func (config *Config) Delete(path string) (error) { + if !config.Layer.Exists(path) { + return errors.New("path not found") + } + + all := config.Layer.All() + + if all == nil { + return errors.New("empty config") + } + + for _, key := range config.Layer.Keys() { + if strings.HasPrefix(key, path + ".") || key == path { + config.Layer.Delete(key) + } + } + + return nil +} + func (config *Config) LoadDir(path string, dir string, ext string, parser koanf.Parser) error { files, err := filepath.Glob(filepath.Join(dir, "*" + ext)) @@ -79,6 +131,8 @@ func (config *Config) LoadDir(path string, dir string, ext string, parser koanf. for _, f := range files { tmp := New() + tmp.OnLoad(config.LoadFunc) + _, err := tmp.LoadFile(f, parser) if err != nil { @@ -92,7 +146,7 @@ func (config *Config) LoadDir(path string, dir string, ext string, parser koanf. path: array, } - return config.Layer.Load(confmap.Provider(wrapper, "."), nil) + return config.Load(wrapper, "") } func (config *Config) LoadEnv() (koanf.Provider, error) { @@ -124,7 +178,7 @@ func (config *Config) TemplateConfig() { } } - config.Layer.Load(confmap.Provider(data, "."), nil) + config.Load(data, "") } func (config *Config) MergeLayers(layers ...*koanf.Koanf) { @@ -133,96 +187,6 @@ func (config *Config) MergeLayers(layers ...*koanf.Koanf) { } } -func (config *Config) NormalizeKeys() { - data := map[string]any{} - - for _, key := range config.Layer.Keys() { - lower := strings.ToLower(key) - - log.Dev("Lowering key: ", key) - - data[lower] = config.Layer.Get(key) - } - - config.Layer.Delete("") - config.Layer.Load(confmap.Provider(data, "."), nil) -} - -// Transforms Children of path -func (config *Config) TransformChildren(path string, transform func(key string, value any) (string, any)) error { - var sub map[string]any - - if !config.Layer.Exists(path) { - return errors.New("invalid path") - } - - err := config.Layer.Unmarshal(path, &sub) - - if err != nil { - return err - } - - transformed := make(map[string]any) - - for key, val := range sub { - newKey, newVal := transform(key, val) - - transformed[newKey] = newVal - } - - config.Layer.Delete(path) - - config.Layer.Load(confmap.Provider(map[string]any{ - path: transformed, - }, "."), nil) - - return nil -} - -// Does the same thing as transformChildren() but does it for each Array Item inside of root and transforms subPath -func (config *Config) TransformChildrenUnderArray(root string, subPath string, transform func(key string, value any) (string, any)) error { - var array []map[string]any - - err := config.Layer.Unmarshal(root, &array) - if err != nil { - return err - } - - transformed := []map[string]any{} - - for _, data := range array { - tmp := New() - - tmp.Layer.Load(confmap.Provider(map[string]any{ - "item": data, - }, "."), nil) - - err := tmp.TransformChildren("item."+subPath, transform) - - if err != nil { - return err - } - - item := tmp.Layer.Get("item") - - if item != nil { - itemMap, ok := item.(map[string]any) - - if ok { - transformed = append(transformed, itemMap) - } - } - } - - config.Layer.Delete(root) - - config.Layer.Load(confmap.Provider(map[string]any{ - root: transformed, - }, "."), nil) - - return nil -} - func (config *Config) NormalizeEnv(key string, value string) (string, any) { key = strings.ToLower(key) key = strings.ReplaceAll(key, "__", ".") diff --git a/utils/configutils/transform.go b/utils/configutils/transform.go new file mode 100644 index 00000000..7b812e08 --- /dev/null +++ b/utils/configutils/transform.go @@ -0,0 +1,199 @@ +package configutils + +import ( + "maps" + "reflect" + "strconv" + "strings" +) + +type TransformTarget struct { + Key string + Transform string + ChildTransform string + Value any +} + +func GetKeyToTransformMap(value any) map[string]TransformTarget { + data := map[string]TransformTarget{} + + if value == nil { + return data + } + + v := reflect.ValueOf(value) + t := reflect.TypeOf(value) + + if t.Kind() == reflect.Ptr { + v = v.Elem() + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + return data + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fieldValue := v.Field(i) + + key := field.Tag.Get("koanf") + if key == "" { + continue + } + + lower := strings.ToLower(key) + + transformTag := field.Tag.Get("transform") + childTransformTag := field.Tag.Get("childtransform") + + data[lower] = TransformTarget{ + Key: lower, + Transform: transformTag, + ChildTransform: childTransformTag, + Value: getValueSafe(fieldValue), + } + + // Recursively walk nested structs + if fieldValue.Kind() == reflect.Struct || (fieldValue.Kind() == reflect.Ptr && fieldValue.Elem().Kind() == reflect.Struct) { + + sub := GetKeyToTransformMap(fieldValue.Interface()) + + for subKey, subValue := range sub { + fullKey := lower + "." + strings.ToLower(subKey) + + data[fullKey] = subValue + } + } + } + + return data +} + +func getValueSafe(value reflect.Value) any { + if !value.IsValid() { + return nil + } + if value.Kind() == reflect.Ptr { + if value.IsNil() { + return nil + } + return getValueSafe(value.Elem()) + } + return value.Interface() +} + +func (config Config) ApplyTransformFuncs(structSchema any, path string, funcs map[string]func(string, any) (string, any)) { + path = getPath(path) + + transformTargets := GetKeyToTransformMap(structSchema) + + data := config.Layer.Get(path) + + _, res := applyTransform("", data, transformTargets, funcs) + + mapRes, ok := res.(map[string]any) + + if !ok { + return + } + + config.Layer.Delete("") + config.Load(mapRes, path) +} + +func applyTransform(key string, value any, transformTargets map[string]TransformTarget, funcs map[string]func(string, any) (string, any)) (string, any) { + lower := strings.ToLower(key) + target := transformTargets[lower] + + targets := map[string]TransformTarget{} + + maps.Copy(targets, transformTargets) + + newKey, _ := applyTransformToAny(lower, value, transformTargets, funcs) + + newKeyWithDot := newKey + + if newKey != "" { + newKeyWithDot = newKey + "." + } + + switch asserted := value.(type) { + case map[string]any: + res := map[string]any{} + + for k, v := range asserted { + fullKey := newKeyWithDot + k + + _, ok := targets[fullKey] + + if !ok { + childTarget := TransformTarget{ + Key: fullKey, + Transform: target.ChildTransform, + ChildTransform: target.ChildTransform, + } + + targets[fullKey] = childTarget + } + + childKey, childValue := applyTransform(fullKey, v, targets, funcs) + + res[childKey] = childValue + } + + return newKey, res + case []any: + res := []any{} + + for i, child := range asserted { + fullKey := newKeyWithDot + strconv.Itoa(i) + + _, ok := targets[fullKey] + + if !ok { + childTarget := TransformTarget{ + Key: fullKey, + Transform: target.ChildTransform, + ChildTransform: target.ChildTransform, + } + + targets[fullKey] = childTarget + } + + _, childValue := applyTransform(fullKey, child, targets, funcs) + + res = append(res, childValue) + } + + return newKey, res + default: + return applyTransformToAny(key, asserted, transformTargets, funcs) + } +} + +func applyTransformToAny(key string, value any, transformTargets map[string]TransformTarget, funcs map[string]func(string, any) (string, any)) (string, any) { + lower := strings.ToLower(key) + + transformTarget, ok := transformTargets[lower] + if !ok { + transformTarget.Transform = "default" + } + + fn, ok := funcs[transformTarget.Transform] + if !ok { + fn = funcs["default"] + } + + keyParts := getKeyParts(key) + + newKey, newValue := fn(keyParts[len(keyParts)-1], value) + + return newKey, newValue +} + +func getKeyParts(fullKey string) []string { + keyParts := strings.Split(fullKey, ".") + + return keyParts +} \ No newline at end of file