diff --git a/v2/glauth.go b/v2/glauth.go index 7829ea7..1e488ad 100644 --- a/v2/glauth.go +++ b/v2/glauth.go @@ -1,13 +1,10 @@ package main import ( - "bytes" "fmt" "net/http" "os" "os/signal" - "path/filepath" - "reflect" "strings" "syscall" "time" @@ -16,6 +13,8 @@ import ( "github.com/arl/statsviz" docopt "github.com/docopt/docopt-go" "github.com/fsnotify/fsnotify" + "github.com/jinzhu/copier" + "github.com/glauth/glauth/v2/internal/toml" "github.com/glauth/glauth/v2/internal/version" "github.com/glauth/glauth/v2/pkg/config" @@ -24,12 +23,7 @@ import ( "github.com/glauth/glauth/v2/pkg/server" "github.com/glauth/glauth/v2/pkg/stats" - // "github.com/hydronica/toml" - - "github.com/jinzhu/copier" "github.com/rs/zerolog" - "gopkg.in/amz.v3/aws" - "gopkg.in/amz.v3/s3" ) const programName = "glauth" @@ -65,6 +59,7 @@ var ( ) func main() { + if err := parseArgs(); err != nil { fmt.Println("Could not parse command-line arguments") fmt.Println(err) @@ -76,15 +71,35 @@ func main() { checkConfig = true } } - if err := doConfig(checkConfig); err != nil { + + cfg, err := toml.NewConfig(checkConfig, getConfigLocation(), args) + + if err != nil { fmt.Println("Configuration file error") fmt.Println(err) os.Exit(1) } + if checkConfig { fmt.Println("Config file seems ok (but I am not checking much at this time)") return } + + if err := copier.Copy(activeConfig, cfg); err != nil { + log.Info().Err(err).Msg("Could not save reloaded config. Holding on to old config") + } + + log = logging.InitLogging(activeConfig.Debug, activeConfig.Syslog, activeConfig.StructuredLog) + + if !checkConfig { + if cfg.Debug { + log.Info().Msg("Debugging enabled") + } + if cfg.Syslog { + log.Info().Msg("Syslog enabled") + } + } + log.Info().Msg("AP start") startService() @@ -195,10 +210,17 @@ func startConfigWatcher() { isChanged, isRemoved = true, false } if isChanged { - if err := doConfig(false); err != nil { + + cfg, err := toml.NewConfig(false, configFileLocation, args) + + if err != nil { log.Info().Err(err).Msg("Could not reload config. Holding on to old config") } else { log.Info().Msg("Config was reloaded") + + if err := copier.Copy(activeConfig, cfg); err != nil { + log.Info().Err(err).Msg("Could not save reloaded config. Holding on to old config") + } } isChanged = false } @@ -222,529 +244,3 @@ func parseArgs() error { func getConfigLocation() string { return args["--config"].(string) } - -func parseConfigFile(configFileLocation string) (*config.Config, error) { - cfg := 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 == false { - 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) - } - - var md toml.MetaData - - 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 md, err = toml.Decode(destbuf.String(), &merged); err != nil { - return &cfg, err - } - cfg = merged - } else { - md, err = toml.DecodeFile(configFileLocation, &cfg) - if err != nil { - return &cfg, err - } - } - - switch users := md.Mappings()["users"].(type) { - case []map[string]interface{}: - for _, mduser := range users { - if mduser["customattributes"] != nil { - for idx, cfguser := range cfg.Users { - if cfguser.Name == mduser["name"].(string) { - switch attributes := mduser["customattributes"].(type) { - case []map[string]interface{}: - cfg.Users[idx].CustomAttrs = attributes[0] - case map[string]interface{}: - cfg.Users[idx].CustomAttrs = attributes - default: - log.Info().Interface("attributes", attributes).Msg("Unknown attribute structure in config file") - } - break - } - } - } - } - } - - } - - // 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" - } - if cfg.Backends[i].GroupFormat == "" { - cfg.Backends[i].GroupFormat = "ou" - } - if cfg.Backends[i].SSHKeyAttr == "" { - cfg.Backends[i].SSHKeyAttr = "sshPublicKey" - } - } - // - - return &cfg, nil -} - -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 mergeConfigsO(config1 interface{}, config2 interface{}) (interface{}, error) { - var merger func(int, string, interface{}, interface{}) (interface{}, error) - merger = func(depth int, keyName string, cfg1 interface{}, cfg2 interface{}) (interface{}, error) { - var returnElement interface{} - fmt.Println(strings.Repeat(" ", depth), "Handling element: ", keyName, " for: ", cfg1) - switch element1 := cfg1.(type) { - case map[string]interface{}: - element2, ok := cfg2.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("config: %s is not a map", keyName) - } - for k, value2 := range element2 { - value1, ok := element1[k] - if !ok { - return nil, fmt.Errorf("config: %s not found in at least one config file", k) - } - merged, err := merger(depth+1, k, value1, value2) - if err != nil { - return merged, err - } - element1[k] = merged - } - returnElement = element1 - case string: - element2, ok := cfg2.(string) - if !ok { - return nil, fmt.Errorf("config: %s is not a string", keyName) - } - if element2 != "" { - element1 = element2 - } - returnElement = element1 - case bool: - element2, ok := cfg2.(bool) - if !ok { - return nil, fmt.Errorf("config: %s is not a boolean value", keyName) - } - if element2 { - element1 = true - } - returnElement = element1 - case float64: - element2, ok := cfg2.(float64) - if !ok { - return nil, fmt.Errorf("config: %s is not a float64 value", keyName) - } - if element2 != 0 { - element1 = element2 - } - returnElement = element1 - case nil: - if cfg2 == nil { - returnElement = nil - } else { - element2, ok := cfg2.(map[string]interface{}) - if ok { - returnElement = element2 - } else { - element3, ok := cfg2.([]interface{}) - if ok { - returnElement = element3 - } else { - log.Info().Msg("Unexpected interface type for an assignment. Ignoring.") - returnElement = element1 - } - } - } - default: - log.Info().Str("type", reflect.TypeOf(element1).String()).Msg("Unknown element type found in configuration file. Ignoring.") - returnElement = element1 - } - fmt.Println(strings.Repeat(" ", depth), "Done with element: ", keyName, " for: ", returnElement) - return returnElement, nil - } - - /* - flattenedConfig1, _ := json.Marshal(config1) - var jsonConfig1 interface{} - //json.Unmarshal(flattenedConfig1, &jsonConfig1) - d1 := json.NewDecoder(bytes.NewReader(flattenedConfig1)) - d1.UseNumber() - if err := d1.Decode(&jsonConfig1); err != nil { - return config1, err - } - - flattenedConfig2, _ := json.Marshal(config2) - var jsonConfig2 interface{} - //json.Unmarshal(flattenedConfig2, &jsonConfig2) - //spew.Dump(jsonConfig2) - d2 := json.NewDecoder(bytes.NewReader(flattenedConfig2)) - d2.UseNumber() - if err := d2.Decode(&jsonConfig2); err != nil { - return config1, err - } - */ - - blocks, err := merger(0, "TOP", config1, config2) - if err != nil { - return config1, err - } - // - // - // TODO: return blocks... we will convert back to toml when we are done merging! - - // - buf := new(bytes.Buffer) - err = toml.NewEncoder(buf).Encode(blocks) - if err != nil { - return config1, err - } - fmt.Println(buf.String()) - merged := config.Config{} - if _, err := toml.Decode(buf.String(), &merged); err != nil { - return config1, err - } - return merged, nil -} - -func handleArgs(cfg config.Config) (*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.Cert) == 0 || len(cfg.LDAPS.Key) == 0 { - return &cfg, fmt.Errorf("LDAPS was enabled but no certificate or key were specified: please disable LDAPS or use the 'cert' and 'key' options") - } - - 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.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") - } - } - - //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": - default: - return &cfg, fmt.Errorf("invalid backend %s - must be 'config', 'ldap', 'owncloud' or 'plugin'", 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 -} - -// doConfig reads the cli flags and config file -func doConfig(checkConfig bool) error { - // Parse config-file into config{} struct - cfg, err := parseConfigFile(getConfigLocation()) - if err != nil { - return err - } - - // Handle parsed flags - cfg, err = handleArgs(*cfg) - if err != nil { - return err - } - - // Handle parsing of legacy [frontend] section into [ldap] and/or [ldaps] sections - cfg, err = handleLegacyConfig(*cfg) - if err != nil { - return err - } - - cfg, err = validateConfig(*cfg) - if err != nil { - return err - } - - // Before greenlighting new config entirely, lets make sure the yubiauth works - in case they changed - if activeConfig.YubikeyClientID != cfg.YubikeyClientID || activeConfig.YubikeySecret != cfg.YubikeySecret { - if len(cfg.YubikeyClientID) > 0 && len(cfg.YubikeySecret) > 0 { - _yubiAuth, err := yubigo.NewYubiAuth(cfg.YubikeyClientID, cfg.YubikeySecret) - if err != nil { - return err - } - - // No errors, override - yubiAuth = _yubiAuth - } - } - - // All config is validated and alright, copy to activeConfig - if err := copier.Copy(activeConfig, cfg); err != nil { - return err - } - - // Handle logging settings for new config - // - we do this last to make sure we only respect a fully validated config - log = logging.InitLogging(activeConfig.Debug, activeConfig.Syslog, activeConfig.StructuredLog) - - if !checkConfig { - if activeConfig.Debug { - log.Info().Msg("Debugging enabled") - } - if activeConfig.Syslog { - log.Info().Msg("Syslog enabled") - } - } - - return nil -}