From b28965b7c794404bb952dd01dabbc020ed35012d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Mon, 6 May 2024 17:21:35 +0200 Subject: [PATCH] Use koanf to read ldap env and files --- go.mod | 5 +- go.sum | 2 + internal/ldap/client.go | 23 +++-- internal/ldap/rc.go | 223 +++++++++++++--------------------------- internal/wanted/map.go | 4 +- 5 files changed, 92 insertions(+), 165 deletions(-) diff --git a/go.mod b/go.mod index 9dd971b9..d30f714d 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/dalibo/ldap2pg go 1.21 -toolchain go1.21.1 - require ( github.com/avast/retry-go/v4 v4.6.0 github.com/deckarep/golang-set/v2 v2.6.0 @@ -11,6 +9,8 @@ require ( github.com/gosimple/slug v1.14.0 github.com/jackc/pgx/v5 v5.5.5 github.com/joho/godotenv v1.5.1 + github.com/knadh/koanf/maps v0.1.1 + github.com/knadh/koanf/providers/confmap v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 github.com/knadh/koanf/providers/posflag v0.1.0 github.com/knadh/koanf/v2 v2.1.1 @@ -34,7 +34,6 @@ require ( github.com/gosimple/unidecode v1.0.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/knadh/koanf/maps v0.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index 4731e4ad..abf0fa7a 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= diff --git a/internal/ldap/client.go b/internal/ldap/client.go index 9d3a5f6b..65bcc0ad 100644 --- a/internal/ldap/client.go +++ b/internal/ldap/client.go @@ -6,6 +6,7 @@ import ( "log/slog" "net" "net/url" + "strings" "time" "github.com/avast/retry-go/v4" @@ -23,18 +24,19 @@ type Client struct { Conn *ldap3.Conn } -func Connect(options OptionsMap) (client Client, err error) { - uris := options.GetStrings("URI") +func Connect() (client Client, err error) { + uri := k.String("URI") + uris := strings.Split(uri, " ") if len(uris) == 0 { err = fmt.Errorf("missing URI") return } t := tls.Config{ - InsecureSkipVerify: options.GetString("TLS_REQCERT") != "try", + InsecureSkipVerify: k.String("TLS_REQCERT") != "try", } d := net.Dialer{ - Timeout: options.GetSeconds("NETWORK_TIMEOUT"), + Timeout: k.Duration("NETWORK_TIMEOUT") * time.Second, } try := 0 err = retry.Do( @@ -60,24 +62,25 @@ func Connect(options OptionsMap) (client Client, err error) { return } - client.Timeout = options.GetSeconds("TIMEOUT") + client.Timeout = k.Duration("TIMEOUT") * time.Second + slog.Debug("LDAP set timeout.", "timeout", client.Timeout) client.Conn.SetTimeout(client.Timeout) - client.SaslMech = options.GetString("SASL_MECH") + client.SaslMech = k.String("SASL_MECH") switch client.SaslMech { case "": - client.BindDN = options.GetString("BINDDN") + client.BindDN = k.String("BINDDN") if client.BindDN == "" { err = fmt.Errorf("missing BINDDN") return } - password := options.GetSecret("PASSWORD") + password := k.String("PASSWORD") client.Password = "*******" slog.Debug("LDAP simple bind.", "binddn", client.BindDN) err = client.Conn.Bind(client.BindDN, password) case "DIGEST-MD5": - client.SaslAuthCID = options.GetString("SASL_AUTHCID") - password := options.GetSecret("PASSWORD") + client.SaslAuthCID = k.String("SASL_AUTHCID") + password := k.String("PASSWORD") var parsedURI *url.URL parsedURI, err = url.Parse(client.URI) if err != nil { diff --git a/internal/ldap/rc.go b/internal/ldap/rc.go index 68ca4435..b68cea35 100644 --- a/internal/ldap/rc.go +++ b/internal/ldap/rc.go @@ -3,194 +3,117 @@ package ldap import ( "bufio" + "errors" "fmt" - "io" "log/slog" "os" "path/filepath" "regexp" - "strconv" "strings" - "time" -) - -var knownOptions = []string{ - "BASE", - "BINDDN", - "PASSWORD", // ldap2pg extension. - "REFERRALS", - "SASL_AUTHCID", - "SASL_AUTHZID", - "SASL_MECH", - "TIMEOUT", - "TLS_REQCERT", - "NETWORK_TIMEOUT", - "URI", -} -// Holds options with their raw value from either file of env. Marshaling is -// done on demand by getters. -type OptionsMap map[string]RawOption + "github.com/knadh/koanf/maps" + "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/v2" +) -type RawOption struct { - Key string - Value string - Origin string -} +var k = koanf.New("_") -func Initialize() (options OptionsMap, err error) { +// cf. https://git.openldap.org/openldap/openldap/-/blob/bf01750381726db3052d94514eec4048c90a616a/libraries/libldap/init.c#L640 +func Initialize() error { _, ok := os.LookupEnv("LDAPNOINIT") if ok { slog.Debug("Skip LDAP initialization.") - return + return nil } - path := "/etc/ldap/ldap.conf" + + _ = k.Load(confmap.Provider(map[string]interface{}{ + "URI": "ldap://localhost", + "NETWORK_TIMEOUT": "30", + "RC": "ldaprc", + "TLS_REQCERT": "try", + "TIMEOUT": "30", + }, "_"), nil) + + _ = k.Load(env.Provider("LDAP", "_", func(key string) string { + return strings.TrimPrefix(key, "LDAP") + }), nil) + + // cf. https://git.openldap.org/openldap/openldap/-/blob/bf01750381726db3052d94514eec4048c90a616a/libraries/libldap/init.c#L741 home, _ := os.UserHomeDir() - options = make(OptionsMap) - options.LoadDefaults() - err = options.LoadFiles( - path, + files := []string{ + "/etc/ldap/ldap.conf", filepath.Join(home, "ldaprc"), filepath.Join(home, ".ldaprc"), - "ldaprc", - ) - if err != nil { - return + "ldaprc", // search in CWD + // Read CONF and RC only from env, before above files are effectively read. + k.String("CONF"), + filepath.Join(home, k.String("RC")), + filepath.Join(home, fmt.Sprintf(".%s", k.String("RC"))), + k.String("RC"), // Search in CWD. } - path = os.Getenv("LDAPCONF") - if "" != path { - err = options.LoadFiles(path) - if err != nil { - return + for _, candidate := range files { + if candidate == "" { + continue } - } - path = os.Getenv("LDAPRC") - if "" != path { - err = options.LoadFiles( - filepath.Join(home, path), - fmt.Sprintf("%s/.%s", home, path), - "./"+path, - ) - } - options.LoadEnv() - return -} -func (m OptionsMap) GetSeconds(name string) time.Duration { - option, ok := m[name] - if ok { - integer, err := strconv.Atoi(option.Value) - if nil == err { - slog.Debug("Read LDAP option.", "key", option.Key, "value", option.Value, "origin", option.Origin) - return time.Duration(integer) * time.Second + err := k.Load(newLooseFileProvider(candidate), parser{}) + if err != nil { + return fmt.Errorf("%s: %w", candidate, err) } - slog.Warn("Bad integer.", "key", name, "value", option.Value, "err", err.Error(), "origin", option.Origin) } - return 0 + return nil } -// Like GetString, but does not log value. -func (m OptionsMap) GetSecret(name string) string { - option, ok := m[name] - if ok { - slog.Debug("Read LDAP option.", "key", option.Key, "origin", option.Origin) - } - return option.Value +// looseFileProvider reads a file if it exists. +type looseFileProvider struct { + path string } -func (m OptionsMap) GetString(name string) string { - option, ok := m[name] - if ok { - slog.Debug("Read LDAP option.", "key", option.Key, "value", option.Value, "origin", option.Origin) +func newLooseFileProvider(path string) koanf.Provider { + if !filepath.IsAbs(path) { + path, _ = filepath.Abs(path) } - return option.Value + return looseFileProvider{path: path} } -func (m OptionsMap) GetStrings(name string) []string { - option, ok := m[name] - if ok { - slog.Debug("Read LDAP option.", "key", option.Key, "value", option.Value, "origin", option.Origin) +func (p looseFileProvider) ReadBytes() ([]byte, error) { + data, err := os.ReadFile(p.path) + if errors.Is(err, os.ErrNotExist) { + return nil, nil } - return strings.Fields(option.Value) + slog.Debug("Found LDAP configuration file.", "path", p.path, "err", err) + return data, err } -func (m *OptionsMap) LoadDefaults() { - defaults := map[string]string{ - "NETWORK_TIMEOUT": "30", - "TLS_REQCERT": "try", - "TIMEOUT": "30", - } - - for key, value := range defaults { - (*m)[key] = RawOption{ - Key: key, - Value: value, - Origin: "default", - } - } +func (looseFileProvider) Read() (map[string]interface{}, error) { + panic("not implemented") } -func (m *OptionsMap) LoadEnv() { - for _, name := range knownOptions { - envName := "LDAP" + name - value, ok := os.LookupEnv(envName) - if !ok { - continue - } - option := RawOption{ - Key: strings.TrimPrefix(envName, "LDAP"), - Value: value, - Origin: "env", - } - (*m)[option.Key] = option - } -} +// parser returns ldaprc as plain map for koanf. +type parser struct{} -func (m *OptionsMap) LoadFiles(path ...string) (err error) { - for _, candidate := range path { - if !filepath.IsAbs(candidate) { - candidate, _ = filepath.Abs(candidate) - } - _, err := os.Stat(candidate) - if err != nil { - slog.Debug("Ignoring configuration file.", "path", candidate, "err", err.Error()) +func (parser) Unmarshal(data []byte) (map[string]interface{}, error) { + out := make(map[string]interface{}) + scanner := bufio.NewScanner(strings.NewReader(string(data))) + re := regexp.MustCompile(`\s+`) + for scanner.Scan() { + line := scanner.Text() + slog.Debug("LDAP config line.", "line", line) + if strings.HasPrefix(line, "#") { continue } - slog.Debug("Found LDAP configuration file.", "path", candidate) - - fo, err := os.Open(candidate) - if err != nil { - return fmt.Errorf("%s: %w", candidate, err) - } - for option := range iterFileOptions(fo) { - option.Origin = candidate - (*m)[option.Key] = option + line = strings.TrimSpace(line) + if "" == line { + continue } + fields := re.Split(line, 2) + slog.Debug("LDAP config line.", "key", fields[0], "value", fields[1]) + out[fields[0]] = fields[1] } - return + return maps.Unflatten(out, "_"), nil } -func iterFileOptions(r io.Reader) <-chan RawOption { - ch := make(chan RawOption) - scanner := bufio.NewScanner(r) - re := regexp.MustCompile(`\s+`) - go func() { - defer close(ch) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "#") { - continue - } - line = strings.TrimSpace(line) - if "" == line { - continue - } - fields := re.Split(line, 2) - ch <- RawOption{ - Key: strings.ToUpper(fields[0]), - Value: fields[1], - } - } - }() - return ch +func (parser) Marshal(map[string]interface{}) ([]byte, error) { + panic("not implemented") } diff --git a/internal/wanted/map.go b/internal/wanted/map.go index ae37cd8a..c8f5238f 100644 --- a/internal/wanted/map.go +++ b/internal/wanted/map.go @@ -49,12 +49,12 @@ func (m Rules) Run(watch *perf.StopWatch, blacklist lists.Blacklist, privileges var errList []error var ldapc ldap.Client if m.HasLDAPSearches() { - ldapOptions, err := ldap.Initialize() + err := ldap.Initialize() if err != nil { return nil, nil, err } - ldapc, err = ldap.Connect(ldapOptions) + ldapc, err = ldap.Connect() if err != nil { return nil, nil, err }