Skip to content

Commit

Permalink
Use koanf to read ldap env and files
Browse files Browse the repository at this point in the history
  • Loading branch information
bersace committed May 6, 2024
1 parent 53f1c88 commit ddc0a02
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 165 deletions.
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ module github.com/dalibo/ldap2pg

go 1.21

toolchain go1.21.1

require (
github.com/avast/retry-go v3.0.0+incompatible
github.com/deckarep/golang-set/v2 v2.6.0
github.com/go-ldap/ldap/v3 v3.4.6
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/file v0.1.0
github.com/knadh/koanf/providers/posflag v0.1.0
github.com/knadh/koanf/v2 v2.1.1
github.com/lithammer/dedent v1.1.0
Expand All @@ -28,13 +29,13 @@ require (
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/google/uuid v1.6.0 // indirect
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
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
Expand All @@ -36,8 +38,12 @@ 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/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U=
github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
Expand Down Expand Up @@ -101,6 +107,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
23 changes: 13 additions & 10 deletions internal/ldap/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log/slog"
"net"
"net/url"
"strings"
"time"

"github.com/avast/retry-go"
Expand All @@ -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(
Expand All @@ -59,24 +61,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 {
Expand Down
223 changes: 73 additions & 150 deletions internal/ldap/rc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Loading

0 comments on commit ddc0a02

Please sign in to comment.