444 lines
10 KiB
Go
444 lines
10 KiB
Go
// 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("<config error> %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("<config error> %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
|
|
}
|