// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package common import ( "encoding/json" "errors" "flag" "fmt" "os" "path/filepath" "runtime" "strings" ucfg "github.com/elastic/go-ucfg" "github.com/elastic/go-ucfg/cfgutil" "github.com/elastic/go-ucfg/yaml" "github.com/elastic/beats/libbeat/common/file" "github.com/elastic/beats/libbeat/logp" ) var flagStrictPerms = flag.Bool("strict.perms", true, "Strict permission checking on config files") // IsStrictPerms returns true if strict permission checking on config files is // enabled. func IsStrictPerms() bool { if !*flagStrictPerms || os.Getenv("BEAT_STRICT_PERMS") == "false" { return false } return true } // Config object to store hierarchical configurations into. // See https://godoc.org/github.com/elastic/go-ucfg#Config type Config ucfg.Config // ConfigNamespace storing at most one configuration section by name and sub-section. type ConfigNamespace struct { name string config *Config } var configOpts = []ucfg.Option{ ucfg.PathSep("."), ucfg.ResolveEnv, ucfg.VarExp, } const ( selectorConfig = "config" selectorConfigWithPassword = "config-with-passwords" ) var debugBlacklist = MakeStringSet( "password", "passphrase", "key_passphrase", "pass", "proxy_url", "url", "urls", "host", "hosts", ) // make hasSelector and configDebugf available for unit testing var hasSelector = logp.HasSelector var configDebugf = logp.Debug func NewConfig() *Config { return fromConfig(ucfg.New()) } // NewConfigFrom creates a new Config object from the given input. // From can be any kind of structured data (struct, map, array, slice). // // If from is a string, the contents is treated like raw YAML input. The string // will be parsed and a structure config object is build from the parsed // result. func NewConfigFrom(from interface{}) (*Config, error) { if str, ok := from.(string); ok { c, err := yaml.NewConfig([]byte(str), configOpts...) return fromConfig(c), err } c, err := ucfg.NewFrom(from, configOpts...) return fromConfig(c), err } // MustNewConfigFrom creates a new Config object from the given input. // From can be any kind of structured data (struct, map, array, slice). // // If from is a string, the contents is treated like raw YAML input. The string // will be parsed and a structure config object is build from the parsed // result. // // MustNewConfigFrom panics if an error occurs. func MustNewConfigFrom(from interface{}) *Config { cfg, err := NewConfigFrom(from) if err != nil { panic(err) } return cfg } func MergeConfigs(cfgs ...*Config) (*Config, error) { config := NewConfig() for _, c := range cfgs { if err := config.Merge(c); err != nil { return nil, err } } return config, nil } func NewConfigWithYAML(in []byte, source string) (*Config, error) { opts := append( []ucfg.Option{ ucfg.MetaData(ucfg.Meta{Source: source}), }, configOpts..., ) c, err := yaml.NewConfig(in, opts...) return fromConfig(c), err } // OverwriteConfigOpts allow to change the globally set config option func OverwriteConfigOpts(options []ucfg.Option) { configOpts = options } func LoadFile(path string) (*Config, error) { if IsStrictPerms() { if err := ownerHasExclusiveWritePerms(path); err != nil { return nil, err } } c, err := yaml.NewConfigWithFile(path, configOpts...) if err != nil { return nil, err } cfg := fromConfig(c) cfg.PrintDebugf("load config file '%v' =>", path) return cfg, err } func LoadFiles(paths ...string) (*Config, error) { merger := cfgutil.NewCollector(nil, configOpts...) for _, path := range paths { cfg, err := LoadFile(path) if err := merger.Add(cfg.access(), err); err != nil { return nil, err } } return fromConfig(merger.Config()), nil } func (c *Config) Merge(from interface{}) error { return c.access().Merge(from, configOpts...) } func (c *Config) Unpack(to interface{}) error { return c.access().Unpack(to, configOpts...) } func (c *Config) Path() string { return c.access().Path(".") } func (c *Config) PathOf(field string) string { return c.access().PathOf(field, ".") } func (c *Config) HasField(name string) bool { return c.access().HasField(name) } func (c *Config) CountField(name string) (int, error) { return c.access().CountField(name) } func (c *Config) Bool(name string, idx int) (bool, error) { return c.access().Bool(name, idx, configOpts...) } func (c *Config) String(name string, idx int) (string, error) { return c.access().String(name, idx, configOpts...) } func (c *Config) Int(name string, idx int) (int64, error) { return c.access().Int(name, idx, configOpts...) } func (c *Config) Float(name string, idx int) (float64, error) { return c.access().Float(name, idx, configOpts...) } func (c *Config) Child(name string, idx int) (*Config, error) { sub, err := c.access().Child(name, idx, configOpts...) return fromConfig(sub), err } func (c *Config) SetBool(name string, idx int, value bool) error { return c.access().SetBool(name, idx, value, configOpts...) } func (c *Config) SetInt(name string, idx int, value int64) error { return c.access().SetInt(name, idx, value, configOpts...) } func (c *Config) SetFloat(name string, idx int, value float64) error { return c.access().SetFloat(name, idx, value, configOpts...) } func (c *Config) SetString(name string, idx int, value string) error { return c.access().SetString(name, idx, value, configOpts...) } func (c *Config) SetChild(name string, idx int, value *Config) error { return c.access().SetChild(name, idx, value.access(), configOpts...) } func (c *Config) IsDict() bool { return c.access().IsDict() } func (c *Config) IsArray() bool { return c.access().IsArray() } func (c *Config) PrintDebugf(msg string, params ...interface{}) { selector := selectorConfigWithPassword filtered := false if !hasSelector(selector) { selector = selectorConfig filtered = true if !hasSelector(selector) { return } } debugStr := configDebugString(c, filtered) if debugStr != "" { configDebugf(selector, "%s\n%s", fmt.Sprintf(msg, params...), debugStr) } } func (c *Config) Enabled() bool { testEnabled := struct { Enabled bool `config:"enabled"` }{true} if c == nil { return false } if err := c.Unpack(&testEnabled); err != nil { // if unpacking fails, expect 'enabled' being set to default value return true } return testEnabled.Enabled } func fromConfig(in *ucfg.Config) *Config { return (*Config)(in) } func (c *Config) access() *ucfg.Config { return (*ucfg.Config)(c) } func (c *Config) GetFields() []string { return c.access().GetFields() } // Unpack unpacks a configuration with at most one sub object. An sub object is // ignored if it is disabled by setting `enabled: false`. If the configuration // passed contains multiple active sub objects, Unpack will return an error. func (ns *ConfigNamespace) Unpack(cfg *Config) error { fields := cfg.GetFields() if len(fields) == 0 { return nil } var ( err error found bool ) for _, name := range fields { var sub *Config sub, err = cfg.Child(name, -1) if err != nil { // element is no configuration object -> continue so a namespace // Config unpacked as a namespace can have other configuration // values as well continue } if !sub.Enabled() { continue } if ns.name != "" { return errors.New("more than one namespace configured") } ns.name = name ns.config = sub found = true } if !found { return err } return nil } // Name returns the configuration sections it's name if a section has been set. func (ns *ConfigNamespace) Name() string { return ns.name } // Config return the sub-configuration section if a section has been set. func (ns *ConfigNamespace) Config() *Config { return ns.config } // IsSet returns true if a sub-configuration section has been set. func (ns *ConfigNamespace) IsSet() bool { return ns.config != nil } func configDebugString(c *Config, filterPrivate bool) string { var bufs []string if c.IsDict() { var content map[string]interface{} if err := c.Unpack(&content); err != nil { return fmt.Sprintf(" %v", err) } if filterPrivate { filterDebugObject(content) } j, _ := json.MarshalIndent(content, "", " ") bufs = append(bufs, string(j)) } if c.IsArray() { var content []interface{} if err := c.Unpack(&content); err != nil { return fmt.Sprintf(" %v", err) } if filterPrivate { filterDebugObject(content) } j, _ := json.MarshalIndent(content, "", " ") bufs = append(bufs, string(j)) } if len(bufs) == 0 { return "" } return strings.Join(bufs, "\n") } func filterDebugObject(c interface{}) { switch cfg := c.(type) { case map[string]interface{}: for k, v := range cfg { if debugBlacklist.Has(k) { if arr, ok := v.([]interface{}); ok { for i := range arr { arr[i] = "xxxxx" } } else { cfg[k] = "xxxxx" } } else { filterDebugObject(v) } } case []interface{}: for _, elem := range cfg { filterDebugObject(elem) } } } // ownerHasExclusiveWritePerms asserts that the current user or root is the // owner of the config file and that the config file is (at most) writable by // the owner or root (e.g. group and other cannot have write access). func ownerHasExclusiveWritePerms(name string) error { if runtime.GOOS == "windows" { return nil } info, err := file.Stat(name) if err != nil { return err } euid := os.Geteuid() fileUID, _ := info.UID() perm := info.Mode().Perm() if fileUID != 0 && euid != fileUID { return fmt.Errorf(`config file ("%v") must be owned by the beat user `+ `(uid=%v) or root`, name, euid) } // Test if group or other have write permissions. if perm&0022 > 0 { nameAbs, err := filepath.Abs(name) if err != nil { nameAbs = name } return fmt.Errorf(`config file ("%v") can only be writable by the `+ `owner but the permissions are "%v" (to fix the permissions use: `+ `'chmod go-w %v')`, name, perm, nameAbs) } return nil }