diff --git a/go.work b/go.work index 9e73efe..e141302 100644 --- a/go.work +++ b/go.work @@ -1,3 +1,3 @@ -go 1.24.0 +go 1.25.0 use ./v2 diff --git a/v2/glauth_test.go b/v2/glauth_test.go index 85c3b06..b65e9fd 100644 --- a/v2/glauth_test.go +++ b/v2/glauth_test.go @@ -1,8 +1,9 @@ package main import ( + "bytes" + "errors" "fmt" - "net" "os" "os/exec" "path/filepath" @@ -10,6 +11,9 @@ import ( "strings" "testing" "time" + + "github.com/glauth/ldap" + "github.com/pquerna/otp/totp" ) type testEnv struct { @@ -60,12 +64,7 @@ func TestProperBuild(t *testing.T) { } func TestConfigBackendRejectsPasswordlessUserBinds(t *testing.T) { - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - port := listener.Addr().(*net.TCPAddr).Port - listener.Close() + port := 3893 configPath := filepath.Join(t.TempDir(), "passwordless.cfg") config := fmt.Sprintf(`debug = false @@ -97,11 +96,11 @@ func TestConfigBackendRejectsPasswordlessUserBinds(t *testing.T) { action = "search" object = "*" `, port) - if err := os.WriteFile(configPath, []byte(config), 0600); err != nil { + if err := os.WriteFile(configPath, []byte(config), 0o600); err != nil { t.Fatal(err) } - svc := startSvc(SD, glauthBinary(), "-c", configPath) + svc := startSvc(t, glauthBinary(), "-c", configPath) defer stopSvc(svc) bindDN := "cn=nopass,dc=glauth,dc=com" @@ -114,7 +113,11 @@ func TestConfigBackendRejectsPasswordlessUserBinds(t *testing.T) { {name: "empty password", password: "", want: "exit status 53"}, } { t.Run(tc.name, func(t *testing.T) { - got := doRunGetFirst(0, "ldapsearch", "-LLL", "-H", fmt.Sprintf("ldap://127.0.0.1:%d", port), "-D", bindDN, "-w", tc.password, "-x", "-b", "dc=glauth,dc=com", "(cn=nopass)") + out, err := doRun("ldapsearch", "-LLL", "-H", fmt.Sprintf("ldap://127.0.0.1:%d", port), "-D", bindDN, "-w", tc.password, "-x", "-b", "dc=glauth,dc=com", "(cn=nopass)") + got := getFirst(out) + if err != nil { + got = err.Error() + } if got != tc.want { t.Fatalf("bind result = %q, want %q", got, tc.want) } @@ -149,7 +152,7 @@ func ldapOnlyConfig(t *testing.T, source string) string { contents = contents[:start] + sectionWithoutLDAPS + contents[end:] path := filepath.Join(t.TempDir(), filepath.Base(source)) - if err := os.WriteFile(path, []byte(contents), 0600); err != nil { + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatal(err) } return path @@ -164,8 +167,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { { Name: "searching for the 'hackers' user", Check: func(t testing.TB) { - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers") - if got, want := out, env.expectedaccount; got != want { + want := env.expectedaccount + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want) } }, @@ -176,8 +185,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { if env.svcdnnogroup == "" { t.SkipNow() } - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdnnogroup, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers") - if got, want := out, env.expectedaccount; got != want { + want := env.expectedaccount + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdnnogroup, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want) } }, @@ -188,8 +203,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { if !env.checkbindUPN { t.SkipNow() } - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", "serviceuser@example.com", "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers") - if got, want := out, env.expectedaccount; got != want { + want := env.expectedaccount + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", "serviceuser@example.com", "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want) } }, @@ -197,8 +218,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { { Name: "querying the root SDE", Check: func(t testing.TB) { - out := doRunGetSecond(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-s", "base", "(objectclass=*)") - if got, want := out, env.expectedinfo; got != want { + want := env.expectedinfo + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-s", "base", "(objectclass=*)") + got := getSecond(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should get some meta information\ngot: %s\nwant: %s", got, want) } }, @@ -209,8 +236,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { if !env.checkanonymousrootDSE { t.SkipNow() } - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-x", "-s", "base", "(objectclass=*)") - if got, want := out, "exit status 50"; got != want { + want := "exit status 50" + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-x", "-s", "base", "(objectclass=*)") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should get error 50\ngot: %s\nwant: %s", got, want) } }, @@ -218,8 +251,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { { Name: "enumerating posix groups", Check: func(t testing.TB) { - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(objectclass=posixgroup)") - if got, want := out, env.expectedgroup; got != want { + want := env.expectedgroup + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(objectclass=posixgroup)") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should get a list starting with the 'superheros' group\ngot: %s\nwant: %s", got, want) } }, @@ -227,8 +266,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { { Name: "searching for members of the 'superheros' group", Check: func(t testing.TB) { - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com)") - if got, want := out, env.expectedfirstaccount; got != want { + want := env.expectedfirstaccount + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com)") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should get a list starting with the 'hackers' user\ngot: %s\nwant: %s", got, want) } }, @@ -236,8 +281,13 @@ func batteryOfTests(t *testing.T, env *testEnv) { { Name: "performing a complex search for members of 'superheros' group", Check: func(t testing.TB) { - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(&(objectClass=*)(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com))") - if got, want := out, env.expectedfirstaccount; got != want { + want := env.expectedfirstaccount + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(&(objectClass=*)(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com))") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { t.Fatalf("should get a list starting with the 'hackers' user\ngot: %s\nwant: %s", got, want) } }, @@ -248,9 +298,21 @@ func batteryOfTests(t *testing.T, env *testEnv) { if !env.checkTOTP { t.SkipNow() } - otpvalue := doRunGetFirst(RD, "oathtool", "--totp", "-b", "-d", "6", "3hnvnk4ycv44glzigd6s25j4dougs3rk") - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret"+otpvalue, "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers") - if got, want := out, env.scopedaccount; got != want { + // We aren't checking that "github.com/pquerna/otp/totp" creates correct values. + // That's what their own tests are for. + // We are checking that the totp code gets through glauth code successfully + otpvalue, err := totp.GenerateCode("3hnvnk4ycv44glzigd6s25j4dougs3rk", time.Now()) + if err != nil { + t.Fatal("Failed to generate totp code:", err) + } + want := env.scopedaccount + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.otpdn, "-w", "mysecret"+otpvalue, "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers") + got := getFirst(out) + if got != want { + if err != nil { + got = err.Error() + } + t.Log(out) t.Fatalf("should find them in in the 'superheros' group\ngot: %s\nwant: %s", got, want) } }, @@ -261,8 +323,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { if !env.checkTOTP { t.SkipNow() } - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers") - if got, want := out, "exit status 49"; got != want { + want := "exit status 49" + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.otpdn, "-w", "mysecret", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should get 'Invalid credentials(49)'\ngot: %s\nwant: %s", got, want) } }, @@ -273,8 +341,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { if !env.checkTOTP { t.SkipNow() } - out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret123456", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers") - if got, want := out, "exit status 49"; got != want { + want := "exit status 49" + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.otpdn, "-w", "mysecret123456", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers") + got := getFirst(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("should get 'Invalid credentials(49)'\ngot: %s\nwant: %s", got, want) } }, @@ -285,8 +359,14 @@ func batteryOfTests(t *testing.T, env *testEnv) { if env.checkemployeetype == "" { t.SkipNow() } - out := doRunGetSecond(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", env.checkemployeetype, "employeetype") - if got, want := out, "employeetype: Intern"; got != want { + want := "employeetype: Intern" + out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", env.checkemployeetype, "employeetype") + got := getSecond(out) + if err != nil { + got = err.Error() + } + if got != want { + t.Log(out) t.Fatalf("type should be 'Intern'\ngot: %s\nwant: %s", got, want) } }, @@ -316,14 +396,15 @@ func TestSampleSimple(t *testing.T) { checkemployeetype: "cn=hackers", } - svc := startSvc(SD, glauthBinary(), "-c", ldapOnlyConfig(t, "sample-simple.cfg")) + svc := startSvc(t, glauthBinary(), "-c", ldapOnlyConfig(t, "sample-simple.cfg")) batteryOfTests(t, &env) stopSvc(svc) } func TestSQLitePlugin(t *testing.T) { - matchingLibrary := doRunGetFirst(RD, "ls", "bin/sqlite.so") - if matchingLibrary != "bin/sqlite.so" { + _, err := os.Stat("bin/sqlite.so") + if err != nil { + t.Log("Unable to find sqlite plugin") t.SkipNow() } @@ -342,14 +423,20 @@ func TestSQLitePlugin(t *testing.T) { checkemployeetype: "", } - svc := startSvc(SD, glauthBinary(), "-c", "pkg/plugins/glauth-sqlite/sample-database.cfg") + svc := startSvc(t, glauthBinary(), "-c", "pkg/plugins/glauth-sqlite/sample-database.cfg") batteryOfTests(t, &env) stopSvc(svc) } func TestLdapInjection(t *testing.T) { - matchingContainers := doRunGetFirst(RD, "sh", "-c", "docker ps | grep ldap-service | wc -l") + out, err := doRun("sh", "-c", "docker ps | grep ldap-service | wc -l") + if err != nil { + t.Log("Unable to find docker container:", out) + t.SkipNow() + } + matchingContainers := getFirst(out) if matchingContainers != "1" { + t.Log("Unable to find docker container:", out) t.SkipNow() } @@ -368,52 +455,79 @@ func TestLdapInjection(t *testing.T) { checkemployeetype: "", } - svc := startSvc(SD, glauthBinary(), "-c", "sample-ldap-injection.cfg") + svc := startSvc(t, glauthBinary(), "-c", "sample-ldap-injection.cfg") batteryOfTests(t, &env) stopSvc(svc) } // -----=============================================================================---- - -const SD = 3 // Start Delay -const RD = 2 // Response Delay - -func startSvc(delay time.Duration, name string, arg ...string) *exec.Cmd { - cmd := exec.Command(name, arg...) - cmd.Start() - if delay != 0 { - time.Sleep(time.Second * delay) +func WaitForPort(host string, timeout time.Duration) error { + start := time.Now() + for { + conn, err := ldap.Dial("tcp", host) + if err == nil { + err = conn.Bind("_", "_") + if err == nil { + conn.Close() + return fmt.Errorf("Successfully authenticated to glauth. This should not happen") + } + e := &ldap.Error{} + if errors.As(err, &e) && e.ResultCode == 49 { + conn.Close() + return nil + } + conn.Close() + // We have an error, it's probably a connection error or an error saying that ldap isn't setup yet + // try again + } + if time.Since(start) > timeout { + return fmt.Errorf("timeout waiting for port %s: %w", host, err) + } + time.Sleep(50 * time.Millisecond) } +} + +func startSvc(t *testing.T, name string, arg ...string) *exec.Cmd { + + cmd := exec.Command(name, arg...) + cmd.Stderr = t.Output() + cmd.Stdout = t.Output() + cmd.Start() + err := WaitForPort("127.0.0.1:3893", time.Second*2) + if err != nil { + t.Fatal("Failed to wait for glauth to start:", err) + } + // the port is listening but the ldap server may not be accepting connections yet + // TODO: make glauth listen as the last part of the initialization that way as soon as it listens it can process connections return cmd } func stopSvc(svc *exec.Cmd) { svc.Process.Kill() + svc.Process.Wait() } -func doRunGetFirst(delay time.Duration, name string, arg ...string) string { - out := strings.SplitN(doRun(delay, name, arg...), "\n", 2) - if out == nil || len(out) < 1 { +func getFirst(output string) string { + out := strings.SplitN(output, "\n", 2) + if len(out) < 1 { return "*fail*" } return out[0] } -func doRunGetSecond(delay time.Duration, name string, arg ...string) string { - out := strings.SplitN(doRun(delay, name, arg...), "\n", 3) - if out == nil || len(out) < 2 { +func getSecond(output string) string { + out := strings.SplitN(output, "\n", 3) + if len(out) < 2 { return "*fail*" } return out[1] } -func doRun(delay time.Duration, name string, arg ...string) string { - out, err := exec.Command(name, arg...).Output() +func doRun(name string, arg ...string) (string, error) { + out, err := exec.Command(name, arg...).CombinedOutput() if err != nil { - return err.Error() + return string(bytes.TrimSpace(out)), err } - if delay != 0 { - time.Sleep(time.Second * delay) - } - return strings.TrimSpace(string(out)) + + return string(bytes.TrimSpace(out)), nil } diff --git a/v2/go.mod b/v2/go.mod index 69a7fc5..da7a85c 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -1,6 +1,6 @@ module github.com/glauth/glauth/v2 -go 1.21 +go 1.25 require ( github.com/BurntSushi/toml v1.3.2 diff --git a/v2/go.sum b/v2/go.sum index 8b06f8f..068e1dd 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -15,6 +15,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=