492 lines
14 KiB
Go
492 lines
14 KiB
Go
package toml
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/GeertJohan/yubigo"
|
|
"github.com/glauth/glauth/v2/pkg/config"
|
|
"github.com/rs/zerolog/log"
|
|
"gopkg.in/amz.v3/aws"
|
|
"gopkg.in/amz.v3/s3"
|
|
)
|
|
|
|
type Config struct {
|
|
Users []toml.Primitive
|
|
}
|
|
|
|
type User struct {
|
|
Name string
|
|
CustomAttributes []toml.Primitive
|
|
}
|
|
|
|
// NewConfig reads the cli flags and config file
|
|
func NewConfig(checkConfig bool, location string, args map[string]interface{}) (*config.Config, error) {
|
|
// Parse config-file into config{} struct
|
|
cfg, err := parseConfigFile(location, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Handle parsed flags
|
|
cfg, err = handleArgs(cfg, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Handle parsing of legacy [frontend] section into [ldap] and/or [ldaps] sections
|
|
cfg, err = handleLegacyConfig(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg, err = validateConfig(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO @shipperizer reinstate this
|
|
// // Before greenlighting new config entirely, lets make sure the yubiauth works - in case they changed
|
|
|
|
if _, err := yubigo.NewYubiAuth(cfg.YubikeyClientID, cfg.YubikeySecret); err != nil && len(cfg.YubikeyClientID) > 0 && len(cfg.YubikeySecret) > 0 {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func parseConfigFile(configFileLocation string, args map[string]interface{}) (*config.Config, error) {
|
|
cfg := new(config.Config)
|
|
// setup defaults
|
|
cfg.LDAP.Enabled = false
|
|
cfg.LDAPS.Enabled = true
|
|
|
|
// parse the config file
|
|
if strings.HasPrefix(configFileLocation, "s3://") {
|
|
region, present := aws.Regions[args["-r"].(string)]
|
|
if !present {
|
|
return cfg, fmt.Errorf("invalid AWS region: %s", args["-r"])
|
|
}
|
|
if args["--aws_endpoint_url"] != nil {
|
|
region = aws.Region{
|
|
Name: "User defined",
|
|
S3Endpoint: args["--aws_endpoint_url"].(string),
|
|
}
|
|
present = true
|
|
}
|
|
auth, err := aws.EnvAuth()
|
|
if err != nil {
|
|
if args["-K"] == nil || args["-S"] == nil {
|
|
return cfg, fmt.Errorf("AWS credentials not found: must use -K and -S flags, or set these env vars:\n\texport AWS_ACCESS_KEY_ID=\"AAA...\"\n\texport AWS_SECRET_ACCESS_KEY=\"BBBB...\"\n")
|
|
}
|
|
auth = aws.Auth{
|
|
AccessKey: args["-K"].(string),
|
|
SecretKey: args["-S"].(string),
|
|
}
|
|
}
|
|
// parse S3 url
|
|
s3url := strings.TrimPrefix(configFileLocation, "s3://")
|
|
parts := strings.SplitN(s3url, "/", 2)
|
|
if len(parts) != 2 {
|
|
return cfg, fmt.Errorf("invalid S3 URL: %s", s3url)
|
|
}
|
|
b, err := s3.New(auth, region).Bucket(parts[0])
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
tomlData, err := b.Get(parts[1])
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
if _, err := toml.Decode(string(tomlData), cfg); err != nil {
|
|
return cfg, err
|
|
}
|
|
} else { // local config file
|
|
fInfo, err := os.Stat(configFileLocation)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("non-existent config path: %s", configFileLocation)
|
|
}
|
|
|
|
if fInfo.IsDir() { // multiple files in a directory
|
|
rawCfgStruct := make(map[string]interface{})
|
|
|
|
// To keep things simple, we are not going to use the default values built in Cfg
|
|
// so far (LDAP.Enabled, LDAPS.Enabled, etc) so do not forget to specify them!
|
|
/*
|
|
sourcebuf := new(bytes.Buffer)
|
|
err = toml.NewEncoder(sourcebuf).Encode(cfg)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
var initialRawCfgStruct interface{}
|
|
if err := toml.Unmarshal(sourcebuf.Bytes(), &initialRawCfgStruct); err != nil {
|
|
return cfg, err
|
|
}
|
|
if err = mergeConfigs(&rawCfgStruct, initialRawCfgStruct); err != nil {
|
|
return cfg, err
|
|
}
|
|
*/
|
|
|
|
files, _ := os.ReadDir(configFileLocation)
|
|
for _, f := range files {
|
|
canonicalName := filepath.Join(configFileLocation, f.Name())
|
|
|
|
bs, _ := os.ReadFile(canonicalName)
|
|
var curRawCfgStruct interface{}
|
|
if err := toml.Unmarshal(bs, &curRawCfgStruct); err != nil {
|
|
return cfg, err
|
|
}
|
|
if err = mergeConfigs(&rawCfgStruct, curRawCfgStruct); err != nil {
|
|
return cfg, err
|
|
}
|
|
}
|
|
|
|
destbuf := new(bytes.Buffer)
|
|
err = toml.NewEncoder(destbuf).Encode(rawCfgStruct)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
fmt.Println(destbuf.String())
|
|
merged := config.Config{}
|
|
if _, err = toml.Decode(destbuf.String(), &merged); err != nil {
|
|
return cfg, err
|
|
}
|
|
cfg = &merged
|
|
} else {
|
|
_, err = toml.DecodeFile(configFileLocation, cfg)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
}
|
|
|
|
usersCustomAttributes(configFileLocation, cfg)
|
|
}
|
|
|
|
// Backward Compability
|
|
if cfg.Backend.Datastore != "" {
|
|
if cfg.Backends != nil {
|
|
return cfg, fmt.Errorf("you cannot specify both [Backend] and [[Backends]] directives in the same configuration ")
|
|
} else {
|
|
cfg.Backends = append(cfg.Backends, cfg.Backend)
|
|
}
|
|
}
|
|
|
|
// Patch with default values where not specified
|
|
for i := range cfg.Backends {
|
|
if cfg.Backends[i].NameFormat == "" {
|
|
cfg.Backends[i].NameFormat = "cn,uid"
|
|
}
|
|
cfg.Backends[i].NameFormatAsArray = strings.Split(cfg.Backends[i].NameFormat, ",")
|
|
if cfg.Backends[i].GroupFormat == "" {
|
|
cfg.Backends[i].GroupFormat = "ou,cn"
|
|
}
|
|
cfg.Backends[i].GroupFormatAsArray = strings.Split(cfg.Backends[i].GroupFormat, ",")
|
|
if cfg.Backends[i].SSHKeyAttr == "" {
|
|
cfg.Backends[i].SSHKeyAttr = "sshPublicKey"
|
|
}
|
|
}
|
|
//
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// usersCustomAttributes changes config passed in by adding extra information coming from the custom attributes
|
|
func usersCustomAttributes(location string, config *config.Config) {
|
|
// TODO @shipperizer deal with multiple files like in line #126
|
|
c := new(Config)
|
|
|
|
md, err := toml.DecodeFile(location, c)
|
|
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("issues parsing users...keep going")
|
|
return
|
|
}
|
|
|
|
for _, u := range c.Users {
|
|
user := new(User)
|
|
md.PrimitiveDecode(u, user)
|
|
|
|
if user.CustomAttributes == nil {
|
|
continue
|
|
}
|
|
|
|
for idx, cUser := range config.Users {
|
|
if cUser.Name != user.Name {
|
|
continue
|
|
}
|
|
|
|
x := make(map[string]interface{})
|
|
|
|
for _, attribute := range user.CustomAttributes {
|
|
_ = md.PrimitiveDecode(attribute, x)
|
|
|
|
for k, v := range x {
|
|
|
|
if config.Users[idx].CustomAttrs == nil {
|
|
config.Users[idx].CustomAttrs = make(map[string]interface{})
|
|
}
|
|
|
|
config.Users[idx].CustomAttrs[k] = v
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func mergeConfigs(config1 interface{}, config2 interface{}) error {
|
|
var merger func(int, string, interface{}, interface{}) error
|
|
merger = func(depth int, keyName string, cfg1 interface{}, cfg2 interface{}) error {
|
|
//fmt.Println(strings.Repeat(" ", depth), "Handling element: ", keyName, " for: ", cfg2)
|
|
switch element2 := cfg2.(type) {
|
|
case map[string]interface{}:
|
|
//fmt.Println(strings.Repeat(" ", depth), " - A map")
|
|
element2, ok := cfg2.(map[string]interface{})
|
|
if !ok {
|
|
return fmt.Errorf("config source: %s is not a map", keyName)
|
|
}
|
|
element1, ok := cfg1.(*map[string]interface{})
|
|
if !ok {
|
|
return fmt.Errorf("config dest: %s is not a map", keyName)
|
|
}
|
|
for k, _ := range element2 {
|
|
//fmt.Println(strings.Repeat(" ", depth), " - key: ", k)
|
|
_, ok := (*element1)[k]
|
|
if !ok {
|
|
(*element1)[k] = element2[k]
|
|
} else {
|
|
//fmt.Println(strings.Repeat(" ", depth), " - merging: ", element2[k])
|
|
asanarrayptr, ok := (*element1)[k].([]map[string]interface{})
|
|
if ok {
|
|
if err := merger(depth+1, k, &asanarrayptr, element2[k]); err != nil {
|
|
return err
|
|
}
|
|
(*element1)[k] = asanarrayptr
|
|
} else {
|
|
asamapptr, ok := (*element1)[k].(map[string]interface{})
|
|
if ok {
|
|
if err := merger(depth+1, k, &asamapptr, element2[k]); err != nil {
|
|
return err
|
|
}
|
|
(*element1)[k] = asamapptr
|
|
} else {
|
|
return fmt.Errorf("config dest: %s does not make a valid map/array ptr", keyName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case []map[string]interface{}:
|
|
//fmt.Println(strings.Repeat(" ", depth), " - An array")
|
|
element2, ok := cfg2.([]map[string]interface{})
|
|
if !ok {
|
|
return fmt.Errorf("config source: %s is not a map array", keyName)
|
|
}
|
|
//fmt.Println(strings.Repeat(" ", depth), " - element2: ", element2)
|
|
element1, ok := cfg1.(*[]map[string]interface{})
|
|
if !ok {
|
|
return fmt.Errorf("config dest: %s is not a map array", keyName)
|
|
}
|
|
//fmt.Println(strings.Repeat(" ", depth), " - element1: ", element1)
|
|
for index, _ := range element2 {
|
|
*element1 = append(*element1, element2[index])
|
|
}
|
|
case string:
|
|
//fmt.Println(strings.Repeat(" ", depth), " - A string")
|
|
element2, ok := cfg2.(string)
|
|
if !ok {
|
|
return fmt.Errorf("config: %s is not a string", keyName)
|
|
}
|
|
case bool:
|
|
//fmt.Println(strings.Repeat(" ", depth), " - A boolean")
|
|
element2, ok := cfg2.(bool)
|
|
if !ok {
|
|
return fmt.Errorf("config: %s is not a boolean value", keyName)
|
|
}
|
|
case float64:
|
|
//fmt.Println(strings.Repeat(" ", depth), " - A float64")
|
|
element2, ok := cfg2.(float64)
|
|
if !ok {
|
|
return fmt.Errorf("config: %s is not a float64 value", keyName)
|
|
}
|
|
case nil:
|
|
//fmt.Println(strings.Repeat(" ", depth), " - Nil")
|
|
default:
|
|
log.Info().Str("type", reflect.TypeOf(element2).String()).Msg("Unknown element type found in configuration file. Ignoring.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err := merger(0, "TOP", config1, config2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleArgs(cfg *config.Config, args map[string]interface{}) (*config.Config, error) {
|
|
// LDAP flags
|
|
if ldap, ok := args["--ldap"].(string); ok && ldap != "" {
|
|
cfg.LDAP.Enabled = true
|
|
cfg.LDAP.Listen = ldap
|
|
}
|
|
|
|
// LDAPS flags
|
|
if ldaps, ok := args["--ldaps"].(string); ok && ldaps != "" {
|
|
cfg.LDAPS.Enabled = true
|
|
cfg.LDAPS.Listen = ldaps
|
|
}
|
|
if ldapsCert, ok := args["--ldaps-cert"].(string); ok && ldapsCert != "" {
|
|
cfg.LDAPS.Cert = ldapsCert
|
|
}
|
|
if ldapsKey, ok := args["--ldaps-key"].(string); ok && ldapsKey != "" {
|
|
cfg.LDAPS.Key = ldapsKey
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func handleLegacyConfig(cfg *config.Config) (*config.Config, error) {
|
|
if len(cfg.Frontend.Listen) > 0 && (len(cfg.LDAP.Listen) > 0 || len(cfg.LDAPS.Listen) > 0) {
|
|
// Both old server-config and new - dont allow
|
|
return cfg, fmt.Errorf("both old and new server-config in use - please remove old format ([frontend]) and migrate to new format ([ldap], [ldaps])")
|
|
}
|
|
|
|
if len(cfg.Frontend.Listen) > 0 {
|
|
// We're going with old format - parse it into new
|
|
log.Info().Msg("Config [frontend] is deprecated - please move to [ldap] and [ldaps] as-per documentation")
|
|
|
|
cfg.LDAP.Enabled = !cfg.Frontend.TLS
|
|
cfg.LDAPS.Enabled = cfg.Frontend.TLS
|
|
|
|
if cfg.Frontend.TLS {
|
|
cfg.LDAPS.Listen = cfg.Frontend.Listen
|
|
} else {
|
|
cfg.LDAP.Listen = cfg.Frontend.Listen
|
|
}
|
|
|
|
if len(cfg.Frontend.Cert) > 0 {
|
|
cfg.LDAPS.Cert = cfg.Frontend.Cert
|
|
}
|
|
if len(cfg.Frontend.Key) > 0 {
|
|
cfg.LDAPS.Key = cfg.Frontend.Key
|
|
}
|
|
}
|
|
return cfg, nil
|
|
}
|
|
func validateConfig(cfg *config.Config) (*config.Config, error) {
|
|
|
|
if !cfg.LDAP.Enabled && !cfg.LDAPS.Enabled {
|
|
return cfg, fmt.Errorf("no server configuration found: please provide either LDAP or LDAPS configuration")
|
|
}
|
|
|
|
if cfg.LDAPS.Enabled {
|
|
// LDAPS enabled - verify requirements (cert, key, listen)
|
|
if len(cfg.LDAPS.Listen) == 0 {
|
|
return cfg, fmt.Errorf("no LDAPS bind address was specified: please disable LDAPS or use the 'listen' option")
|
|
}
|
|
|
|
if cfg.LDAPS.Cert == "" && cfg.LDAPS.CertPath != "" {
|
|
byteData, err := os.ReadFile(cfg.LDAPS.CertPath)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("unable to read TLS certificate file")
|
|
}
|
|
cfg.LDAPS.Cert = string(byteData)
|
|
}
|
|
|
|
if cfg.LDAPS.Key == "" && cfg.LDAPS.KeyPath != "" {
|
|
byteData, err := os.ReadFile(cfg.LDAPS.KeyPath)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("unable to read TLS key file")
|
|
}
|
|
cfg.LDAPS.Key = string(byteData)
|
|
}
|
|
|
|
// Ugly wart for backward compatibility
|
|
// In the olden times, we would simply say "please read from this file"
|
|
// This is now the role of the Path files.
|
|
if !strings.HasPrefix(cfg.LDAPS.Cert, "-----") {
|
|
byteData, err := os.ReadFile(cfg.LDAPS.Cert)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("unable to read TLS certificate file")
|
|
}
|
|
cfg.LDAPS.Cert = string(byteData)
|
|
}
|
|
if !strings.HasPrefix(cfg.LDAPS.Key, "-----") {
|
|
byteData, err := os.ReadFile(cfg.LDAPS.Key)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("unable to read TLS key file")
|
|
}
|
|
cfg.LDAPS.Key = string(byteData)
|
|
}
|
|
|
|
if cfg.LDAPS.Cert == "" || cfg.LDAPS.Key == "" {
|
|
return cfg, fmt.Errorf("LDAPS was enabled but no certificate or key were specified: please disable LDAPS or use the 'cert'/'key' or 'certpath'/'keypath' options")
|
|
}
|
|
}
|
|
|
|
if cfg.LDAP.Enabled {
|
|
// LDAP enabled - verify listen
|
|
if len(cfg.LDAP.Listen) == 0 {
|
|
return cfg, fmt.Errorf("no LDAP bind address was specified: please disable LDAP or use the 'listen' option")
|
|
}
|
|
|
|
if cfg.LDAP.TLS {
|
|
if cfg.LDAP.TLSCert == "" && cfg.LDAP.TLSCertPath != "" {
|
|
byteData, err := os.ReadFile(cfg.LDAP.TLSCertPath)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("unable to read TLS certificate file")
|
|
}
|
|
cfg.LDAP.TLSCert = string(byteData)
|
|
}
|
|
|
|
if cfg.LDAP.TLSKey == "" && cfg.LDAP.TLSKeyPath != "" {
|
|
byteData, err := os.ReadFile(cfg.LDAP.TLSKeyPath)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("unable to read TLS key file")
|
|
}
|
|
cfg.LDAP.TLSKey = string(byteData)
|
|
}
|
|
|
|
if cfg.LDAP.TLSCert == "" || cfg.LDAP.TLSKey == "" {
|
|
return cfg, fmt.Errorf("StartTLS was enabled but no certificate or key were specified: please disable StartTLS or use the 'tlscert'/'tlskey' or 'tlscertpath'/'tlskeypath' options")
|
|
}
|
|
}
|
|
}
|
|
|
|
//spew.Dump(cfg)
|
|
for i := range cfg.Backends {
|
|
switch cfg.Backends[i].Datastore {
|
|
case "":
|
|
cfg.Backends[i].Datastore = "config"
|
|
case "config":
|
|
case "ldap":
|
|
case "owncloud":
|
|
case "plugin":
|
|
case "embed":
|
|
default:
|
|
return cfg, fmt.Errorf("invalid backend %s - must be 'config', 'ldap', 'owncloud', 'plugin' or 'embed", cfg.Backends[i].Datastore)
|
|
}
|
|
}
|
|
|
|
// TODO: remove after deprecating UnixID on User and Group
|
|
for _, user := range cfg.Users {
|
|
if user.UnixID != 0 {
|
|
user.UIDNumber = user.UnixID
|
|
log.Info().Msg(fmt.Sprintf("User '%s': 'unixid' is deprecated - please move to 'uidnumber' as per documentation", user.Name))
|
|
}
|
|
}
|
|
for _, group := range cfg.Groups {
|
|
if group.UnixID != 0 {
|
|
group.GIDNumber = group.UnixID
|
|
log.Info().Msg(fmt.Sprintf("Group '%s': 'unixid' is deprecated - please move to 'gidnumber' as per documentation", group.Name))
|
|
}
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|