palourde/uchiwa

View on GitHub
uchiwa/config/config.go

Summary

Maintainability
B
4 hrs
Test Coverage
package config

import (
    "crypto/tls"
    "encoding/json"
    "fmt"
    "math/rand"
    "os"
    "path/filepath"
    "strings"
    "strconv"

    "github.com/palourde/mergo"
    "github.com/sensu/uchiwa/uchiwa/authentication"
    "github.com/sensu/uchiwa/uchiwa/logger"
)

const obfuscatedValue = "*****"

var (
    defaultGlobalConfig = GlobalConfig{
        Audit: Audit{
            Level:   "default",
            Logfile: "/var/log/sensu/sensu-enterprise-dashboard-audit.log",
        },
        Host: "0.0.0.0",
        Ldap: Ldap{
            LdapServer: LdapServer{
                Port:                 389,
                Security:             "none",
                UserAttribute:        "sAMAccountName",
                UserObjectClass:      "person",
                GroupMemberAttribute: "member",
                GroupObjectClass:     "groupOfNames",
            },
        },
        LogLevel: "info",
        Port:     3000,
        Refresh:  10,
        SSL: SSL{
            TLSMinVersion: "tls10",
        },
        UsersOptions: UsersOptions{
            DateFormat:             "YYYY-MM-DD HH:mm:ss",
            DefaultTheme:           "uchiwa-default",
            DisableNoExpiration:    false,
            RequireSilencingReason: false,
            SilenceDurations:       []float32{0.25, 1, 24},
        },
    }
    defaultSensuConfig = SensuConfig{
        Port:    4567,
        Timeout: 10,
    }
    defaultConfig = Config{
        Uchiwa: defaultGlobalConfig,
    }
    // Private contains the private configuration
    Private *Config
)

// Load retrieves the Uchiwa configuration from files and directories
// and returns the private configuration as a Config struct pointer
func Load(file, directories string) *Config {
    // Load the configuration file
    var err error
    Private, err = loadFile(file)
    if err != nil {
        logger.Fatal(err)
    }

    // Apply default configs to the configuration file
    if err := mergo.Merge(Private, defaultConfig); err != nil {
        logger.Fatal(err)
    }
    for i := range Private.Sensu {
        if err := mergo.Merge(&Private.Sensu[i], defaultSensuConfig); err != nil {
            logger.Fatal(err)
        }
    }

    if directories != "" {
        configDir := loadDirectories(directories)
        // Overwrite the file config with the configs from the directories
        if err := mergo.MergeWithOverwrite(Private, configDir); err != nil {
            logger.Fatal(err)
        }
    }

    Private.Sensu = initSensu(Private.Sensu)

    // Support the dashboard attribute
    if Private.Dashboard != nil {
        Private.Uchiwa = *Private.Dashboard
        // Apply the default config to the dashboard attribute
        if err := mergo.Merge(Private, defaultConfig); err != nil {
            logger.Fatal(err)
        }
    }

    Private.Uchiwa = initUchiwa(Private.Uchiwa)
    return Private
}

// loadDirectories loads a Config struct from one or multiple directories of configuration
func loadDirectories(path string) *Config {
    conf := new(Config)
    var configFiles []string
    directories := strings.Split(strings.ToLower(path), ",")

    for _, directory := range directories {
        // Find all JSON files in the specified directories
        files, err := filepath.Glob(filepath.Join(directory, "*.json"))
        if err != nil {
            logger.Warning(err)
            continue
        }

        // Add the files found to a slice of configuration files to open
        for _, file := range files {
            configFiles = append(configFiles, file)
        }
    }

    // Load every configuration files and merge them together bit by bit
    for _, file := range configFiles {
        // Load the config from the file
        c, err := loadFile(file)
        if err != nil {
            logger.Warning(err)
            continue
        }

        // Apply this configuration to the existing one
        if err := mergo.MergeWithOverwrite(conf, c); err != nil {
            logger.Warning(err)
            continue
        }
    }

    // Apply the default config to the Sensu APIs
    for i := range conf.Sensu {
        if err := mergo.Merge(&conf.Sensu[i], defaultSensuConfig); err != nil {
            logger.Fatal(err)
        }
    }

    return conf
}

// loadFile loads a Config struct from a configuration file
func loadFile(path string) (*Config, error) {
    logger.Warningf("Loading the configuration file %s", path)

    c := new(Config)
    file, err := os.Open(path)
    if err != nil {
        if len(path) > 1 {
            return nil, fmt.Errorf("Error: could not read config file %s.", path)
        }
    }

    decoder := json.NewDecoder(file)
    err = decoder.Decode(c)
    if err != nil {
        return nil, fmt.Errorf("Error decoding file %s: %s", path, err)
    }

    return c, nil
}

func initSensu(apis []SensuConfig) []SensuConfig {
    for i, api := range apis {
        // Set a datacenter name if missing
        if api.Name == "" {
            logger.Warningf("Sensu API %s has no name property, make sure to set it in your configuration. Generating a temporary one...", api.URL)
            apis[i].Name = fmt.Sprintf("sensu-%v", rand.Intn(100))
        }

        // Escape special characters in DC name
        r := strings.NewReplacer(":", "", "/", "", ";", "", "?", "")
        apis[i].Name = r.Replace(apis[i].Name)

        // Make sure the host is not empty
        if api.Host == "" {
            logger.Fatalf("Sensu API %q Host is missing", api.Name)
        }

        // Determine the protocol to use
        prot := "http"
        if api.Ssl {
            prot += "s"
        }

        // Set the API URL
        apis[i].URL = fmt.Sprintf("%s://%s:%d%s", prot, api.Host, api.Port, api.Path)
    }
    return apis
}

func initUchiwa(global GlobalConfig) GlobalConfig {

    // Set the proper authentication driver
    if global.Github.Server != "" {
        global.Auth.Driver = "github"

        for i := range global.Github.Roles {
            authentication.Roles = append(authentication.Roles, global.Github.Roles[i])
        }
    } else if global.Gitlab.Server != "" {
        global.Auth.Driver = "gitlab"

        for i := range global.Gitlab.Roles {
            authentication.Roles = append(authentication.Roles, global.Gitlab.Roles[i])
        }
    } else if global.Ldap.Server != "" || len(global.Ldap.Servers) >= 1 {
        initLdap(&global.Ldap)
        global.Auth.Driver = "ldap"

        for i := range global.Ldap.Roles {
            authentication.Roles = append(authentication.Roles, global.Ldap.Roles[i])
        }
    } else if global.OIDC.Server != "" {
        global.Auth.Driver = "oidc"

        for i := range global.OIDC.Roles {
            authentication.Roles = append(authentication.Roles, global.OIDC.Roles[i])
        }
    } else if global.Db.Driver != "" && global.Db.Scheme != "" {
        global.Auth.Driver = "sql"
    } else if len(global.Users) != 0 {
        logger.Debug("Loading multiple users from the config")
        global.Auth.Driver = "simple"

        for i := range global.Users {
            if global.Users[i].AccessToken != "" {
                global.Users[i].Role.AccessToken = global.Users[i].AccessToken
            }
            if global.Users[i].Readonly != false {
                global.Users[i].Role.Readonly = global.Users[i].Readonly
            }
            authentication.Roles = append(authentication.Roles, global.Users[i].Role)
        }
    } else if global.User != "" && global.Pass != "" {
        logger.Debug("Loading single user from the config")
        global.Auth.Driver = "simple"

        // Support multiple users
        global.Users = append(global.Users, authentication.User{Username: global.User, Password: global.Pass, FullName: global.User})
    }

    // TLS configuration
    var cipherSuite []uint16
    if len(global.SSL.CipherSuite) == 0 {
        cipherSuite = defaultCipherSuite()
    } else {
        cipherSuite = parseCipherSuite(global.SSL.CipherSuite)
    }

    global.SSL.TLSConfig = &tls.Config{
        MinVersion:               TLSVersions[global.SSL.TLSMinVersion],
        MaxVersion:               tls.VersionTLS12,
        CipherSuites:             cipherSuite,
        PreferServerCipherSuites: true,
    }

    // Set the logger level
    logger.SetLogLevel(global.LogLevel)

    // Set the port from the environment if available
    port, ok := os.LookupEnv("PORT")
    
    if ok {
        p, err := strconv.Atoi(port)

        if err != nil {
            logger.Warning(err)
        } else {
            global.Port = p
        }
    }

    // Initialize the users options
    // Set the refresh rate for frontend
    global.UsersOptions.Refresh = global.Refresh * 1000

    return global
}

func initLdap(conf *Ldap) {
    // If we have a server defined directly in the Ldap struct, move it to the
    // Servers slice
    if conf.Server != "" && len(conf.Servers) == 0 {
        conf.Servers = append(conf.Servers, conf.LdapServer)
    }

    // Apply the default config to every LDAP server
    for i := range conf.Servers {
        if conf.Servers[i].Server == "" {
            logger.Fatal("Every LDAP server must have an address configured with the server attribute")
        }

        if conf.Servers[i].GroupBaseDN == "" {
            conf.Servers[i].GroupBaseDN = conf.Servers[i].BaseDN
        }

        if conf.Servers[i].UserBaseDN == "" {
            conf.Servers[i].UserBaseDN = conf.Servers[i].BaseDN
        }

        if err := mergo.Merge(&conf.Servers[i], defaultGlobalConfig.Ldap.LdapServer); err != nil {
            logger.Fatal(err)
        }
    }
}

// GetPublic generates the public configuration
func (c *Config) GetPublic() *Config {
    p := new(Config)
    p.Uchiwa = c.Uchiwa
    p.Uchiwa.User = obfuscatedValue
    p.Uchiwa.Pass = obfuscatedValue
    p.Uchiwa.Users = []authentication.User{}
    p.Uchiwa.Db.Scheme = obfuscatedValue
    p.Uchiwa.Github.ClientID = obfuscatedValue
    p.Uchiwa.Github.ClientSecret = obfuscatedValue
    p.Uchiwa.Gitlab.ClientID = obfuscatedValue
    p.Uchiwa.Gitlab.ClientSecret = obfuscatedValue
    p.Uchiwa.Ldap.BindPass = obfuscatedValue
    p.Uchiwa.OIDC.ClientID = obfuscatedValue
    p.Uchiwa.OIDC.ClientSecret = obfuscatedValue

    for i := range p.Uchiwa.Github.Roles {
        p.Uchiwa.Github.Roles[i].AccessToken = obfuscatedValue
    }

    for i := range p.Uchiwa.Gitlab.Roles {
        p.Uchiwa.Gitlab.Roles[i].AccessToken = obfuscatedValue
    }

    for i := range p.Uchiwa.Ldap.Roles {
        p.Uchiwa.Ldap.Roles[i].AccessToken = obfuscatedValue
    }

    p.Uchiwa.Ldap.Servers = make([]LdapServer, len(c.Uchiwa.Ldap.Servers))
    for i := range c.Uchiwa.Ldap.Servers {
        p.Uchiwa.Ldap.Servers[i] = c.Uchiwa.Ldap.Servers[i]
        p.Uchiwa.Ldap.Servers[i].BindPass = obfuscatedValue
    }

    for i := range p.Uchiwa.OIDC.Roles {
        p.Uchiwa.OIDC.Roles[i].AccessToken = obfuscatedValue
    }

    p.Sensu = make([]SensuConfig, len(c.Sensu))
    for i := range c.Sensu {
        p.Sensu[i] = c.Sensu[i]
        p.Sensu[i].User = obfuscatedValue
        p.Sensu[i].Pass = obfuscatedValue
    }

    return p
}