Files
glauth/v2/glauth_test.go

420 lines
13 KiB
Go

package main
import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
type testEnv struct {
checkanonymousrootDSE bool
checkTOTP bool
checkbindUPN bool
svcdn string
svcdnnogroup string
otpdn string
expectedinfo string
expectedaccount string
scopedaccount string
expectedfirstaccount string
expectedgroup string
checkemployeetype string
}
func glauthBinary() string {
if binary := os.Getenv("GLAUTH_TEST_BINARY"); binary != "" {
return binary
}
qualified := filepath.Join("bin", runtime.GOOS+runtime.GOARCH, "glauth")
if _, err := os.Stat(qualified); err == nil {
return qualified
}
distribution := filepath.Join("bin", "glauth-"+runtime.GOOS+"-"+runtime.GOARCH)
if runtime.GOOS == "windows" {
distribution += ".exe"
}
if _, err := os.Stat(distribution); err == nil {
return distribution
}
return "bin/glauth"
}
func TestProperBuild(t *testing.T) {
info, err := os.Stat(glauthBinary())
if err != nil {
t.Fatal(err)
}
mode := uint32(info.Mode())
if mode&0b001001001 == 0 {
t.Fatalf("bad file mode: %b", mode)
}
}
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()
configPath := filepath.Join(t.TempDir(), "passwordless.cfg")
config := fmt.Sprintf(`debug = false
[ldap]
enabled = true
listen = "127.0.0.1:%d"
tls = false
[ldaps]
enabled = false
[backend]
datastore = "config"
baseDN = "dc=glauth,dc=com"
[behaviors]
LimitFailedBinds = false
[[groups]]
name = "svc"
gidnumber = 5501
[[users]]
name = "nopass"
uidnumber = 5001
primarygroup = 5501
[[users.capabilities]]
action = "search"
object = "*"
`, port)
if err := os.WriteFile(configPath, []byte(config), 0600); err != nil {
t.Fatal(err)
}
svc := startSvc(SD, glauthBinary(), "-c", configPath)
defer stopSvc(svc)
bindDN := "cn=nopass,dc=glauth,dc=com"
for _, tc := range []struct {
name string
password string
want string
}{
{name: "arbitrary password", password: "definitely-wrong", want: "exit status 49"},
{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)")
if got != tc.want {
t.Fatalf("bind result = %q, want %q", got, tc.want)
}
})
}
}
func ldapOnlyConfig(t *testing.T, source string) string {
t.Helper()
data, err := os.ReadFile(source)
if err != nil {
t.Fatal(err)
}
contents := string(data)
sectionMarker := "\n[ldaps]\n"
markerStart := strings.Index(contents, sectionMarker)
if markerStart < 0 {
t.Fatalf("%s has no [ldaps] section", source)
}
start := markerStart + 1
relEnd := strings.Index(contents[start+len("[ldaps]"):], "\n[")
if relEnd < 0 {
t.Fatalf("%s has no section after [ldaps]", source)
}
end := start + len("[ldaps]") + relEnd
section := contents[start:end]
sectionWithoutLDAPS := strings.Replace(section, "enabled = true", "enabled = false", 1)
if section == sectionWithoutLDAPS {
t.Fatalf("%s [ldaps] section does not enable LDAPS", source)
}
contents = contents[:start] + sectionWithoutLDAPS + contents[end:]
path := filepath.Join(t.TempDir(), filepath.Base(source))
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
t.Fatal(err)
}
return path
}
func batteryOfTests(t *testing.T, env *testEnv) {
tests := []struct {
Name string
Path string
Check func(t testing.TB)
}{
{
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 {
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
},
{
Name: "searching for the 'hackers' user without binding with a group",
Check: func(t testing.TB) {
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 {
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
},
{
Name: "searching for the 'hackers' user after binding using the account's UPN",
Check: func(t testing.TB) {
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 {
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
},
{
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 {
t.Fatalf("should get some meta information\ngot: %s\nwant: %s", got, want)
}
},
},
{
Name: "querying the root SDE anonymously without authorizing in config file",
Check: func(t testing.TB) {
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 {
t.Fatalf("should get error 50\ngot: %s\nwant: %s", got, want)
}
},
},
{
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 {
t.Fatalf("should get a list starting with the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
},
{
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 {
t.Fatalf("should get a list starting with the 'hackers' user\ngot: %s\nwant: %s", got, want)
}
},
},
{
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 {
t.Fatalf("should get a list starting with the 'hackers' user\ngot: %s\nwant: %s", got, want)
}
},
},
{
Name: "searching for the 'hacker' user using a TOTP-enabled account",
Check: func(t testing.TB) {
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 {
t.Fatalf("should find them in in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
},
{
Name: "searching for the 'hacker' user using a TOTP-enabled account and no value",
Check: func(t testing.TB) {
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 {
t.Fatalf("should get 'Invalid credentials(49)'\ngot: %s\nwant: %s", got, want)
}
},
},
{
Name: "searching for the 'hacker' user using a TOTP-enabled account and the wrong value",
Check: func(t testing.TB) {
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 {
t.Fatalf("should get 'Invalid credentials(49)'\ngot: %s\nwant: %s", got, want)
}
},
},
{
Name: "searching for the 'hacker' user",
Check: func(t testing.TB) {
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 {
t.Fatalf("type should be 'Intern'\ngot: %s\nwant: %s", got, want)
}
},
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
tc.Check(t)
})
}
}
func TestSampleSimple(t *testing.T) {
env := testEnv{
checkanonymousrootDSE: true,
checkTOTP: true,
checkbindUPN: true,
expectedinfo: "supportedLDAPVersion: 3",
svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com",
svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com",
otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com",
expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com",
expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com",
checkemployeetype: "cn=hackers",
}
svc := startSvc(SD, 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" {
t.SkipNow()
}
env := testEnv{
checkanonymousrootDSE: true,
checkTOTP: false,
checkbindUPN: true,
expectedinfo: "supportedLDAPVersion: 3",
svcdn: "cn=serviceuser,ou=svcaccts,dc=glauth,dc=com",
svcdnnogroup: "cn=serviceuser,dc=glauth,dc=com",
otpdn: "cn=otpuser,ou=superheros,dc=glauth,dc=com",
expectedaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
scopedaccount: "dn: cn=hackers,ou=superheros,dc=glauth,dc=com",
expectedfirstaccount: "dn: cn=hackers,ou=superheros,ou=users,dc=glauth,dc=com",
expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com",
checkemployeetype: "",
}
svc := startSvc(SD, 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")
if matchingContainers != "1" {
t.SkipNow()
}
env := testEnv{
checkanonymousrootDSE: false,
checkTOTP: true,
checkbindUPN: false,
expectedinfo: "objectClass: top",
svcdn: "cn=serviceuser,cn=svcaccts,ou=users,dc=glauth,dc=com",
svcdnnogroup: "", // ignore
otpdn: "cn=otpuser,cn=superheros,ou=users,dc=glauth,dc=com",
expectedaccount: "dn: cn=hackers,cn=superheros,ou=users,dc=glauth,dc=com",
scopedaccount: "dn: cn=hackers,cn=superheros,ou=users,dc=glauth,dc=com",
expectedfirstaccount: "dn: cn=johndoe,cn=superheros,ou=users,dc=glauth,dc=com",
expectedgroup: "dn: ou=superheros,ou=users,dc=glauth,dc=com",
checkemployeetype: "",
}
svc := startSvc(SD, 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)
}
return cmd
}
func stopSvc(svc *exec.Cmd) {
svc.Process.Kill()
}
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 {
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 {
return "*fail*"
}
return out[1]
}
func doRun(delay time.Duration, name string, arg ...string) string {
out, err := exec.Command(name, arg...).Output()
if err != nil {
return err.Error()
}
if delay != 0 {
time.Sleep(time.Second * delay)
}
return strings.TrimSpace(string(out))
}