Rewrite LDAP tests to use Docker

This commit is contained in:
Mark Sagi-Kazar
2019-12-08 20:21:28 +01:00
parent 532c120ba7
commit d2095bb2d8
14 changed files with 165 additions and 3963 deletions

View File

@@ -1,20 +1,18 @@
package ldap
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path/filepath"
"sync"
"testing"
"text/template"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/sirupsen/logrus"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/dexidp/dex/connector"
)
@@ -50,12 +48,6 @@ type subtest struct {
func TestQuery(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
@@ -125,12 +117,6 @@ userpassword: bar
func TestQueryWithEmailSuffix(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
@@ -187,12 +173,6 @@ userpassword: bar
func TestUserFilter(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=Seattle,dc=example,dc=org
objectClass: organizationalUnit
ou: Seattle
@@ -283,12 +263,6 @@ userpassword: bar
func TestGroupQuery(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
@@ -371,12 +345,6 @@ member: cn=jane,ou=People,dc=example,dc=org
func TestGroupsOnUserEntity(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
@@ -468,12 +436,6 @@ gidNumber: 1002
func TestGroupFilter(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
@@ -574,12 +536,6 @@ member: cn=jane,ou=People,dc=example,dc=org
func TestStartTLS(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
@@ -617,12 +573,6 @@ userpassword: foo
func TestInsecureSkipVerify(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
@@ -660,12 +610,6 @@ userpassword: foo
func TestLDAPS(t *testing.T) {
schema := `
dn: dc=example,dc=org
objectClass: dcObject
objectClass: organization
o: Example Company
dc: example
dn: ou=People,dc=example,dc=org
objectClass: organizationalUnit
ou: People
@@ -729,8 +673,7 @@ func TestUsernamePrompt(t *testing.T) {
// runTests runs a set of tests against an LDAP schema. It does this by
// setting up an OpenLDAP server and injecting the provided scheme.
//
// The tests require the slapd and ldapadd binaries available in the host
// machine's PATH.
// The tests require Docker.
//
// The DEX_LDAP_TESTS must be set to "1"
func runTests(t *testing.T, schema string, connMethod connectionMethod, config *Config, tests []subtest) {
@@ -738,12 +681,6 @@ func runTests(t *testing.T, schema string, connMethod connectionMethod, config *
t.Skipf("%s not set. Skipping test (run 'export %s=1' to run tests)", envVar, envVar)
}
for _, cmd := range []string{"slapd", "ldapadd"} {
if _, err := exec.LookPath(cmd); err != nil {
t.Errorf("%s not available", cmd)
}
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
@@ -755,100 +692,65 @@ func runTests(t *testing.T, schema string, connMethod connectionMethod, config *
}
defer os.RemoveAll(tempDir)
configBytes := new(bytes.Buffer)
data := tmplData{
TempDir: tempDir,
Includes: includes(t, wd),
}
data.TLSCertPath, data.TLSKeyPath = tlsAssets(t, wd)
if err := slapdConfigTmpl.Execute(configBytes, data); err != nil {
schemaPath := filepath.Join(tempDir, "schema.ldif")
if err := ioutil.WriteFile(schemaPath, []byte(schema), 0777); err != nil {
t.Fatal(err)
}
configPath := filepath.Join(tempDir, "ldap.conf")
if err := ioutil.WriteFile(configPath, configBytes.Bytes(), 0644); err != nil {
t.Fatal(err)
}
schemaPath := filepath.Join(tempDir, "schema.ldap")
if err := ioutil.WriteFile(schemaPath, []byte(schema), 0644); err != nil {
t.Fatal(err)
req := testcontainers.ContainerRequest{
Image: "osixia/openldap:1.3.0",
ExposedPorts: []string{"389/tcp", "636/tcp"},
Cmd: []string{"--copy-service"},
Env: map[string]string{
"LDAP_BASE_DN": "dc=example,dc=org",
"LDAP_TLS": "true",
"LDAP_TLS_VERIFY_CLIENT": "try",
},
BindMounts: map[string]string{
filepath.Join(wd, "testdata", "certs"): "/container/service/slapd/assets/certs",
schemaPath: "/container/service/slapd/assets/config/bootstrap/ldif/99-schema.ldif",
},
WaitingFor: wait.ForAll(
wait.ForLog("slapd starting").WithOccurrence(3).WithStartupTimeout(time.Minute),
wait.ForListeningPort("389/tcp"),
wait.ForListeningPort("636/tcp"),
),
}
socketPath := url.QueryEscape(filepath.Join(tempDir, "ldap.unix"))
ctx := context.Background()
slapdOut := new(bytes.Buffer)
slapd, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
if slapd != nil {
logs, err := slapd.Logs(ctx)
if err == nil {
defer logs.Close()
cmd := exec.Command(
"slapd",
"-d", "any",
"-h", "ldap://localhost:10389/ ldaps://localhost:10636/ ldapi://"+socketPath,
"-f", configPath,
)
cmd.Stdout = slapdOut
cmd.Stderr = slapdOut
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
var (
// Wait group finishes once slapd has exited.
//
// Use a wait group because multiple goroutines can't listen on
// cmd.Wait(). It triggers the race detector.
wg = new(sync.WaitGroup)
// Ensure only one condition can set the slapdFailed boolean.
once = new(sync.Once)
slapdFailed bool
)
wg.Add(1)
go func() { cmd.Wait(); wg.Done() }()
defer func() {
if slapdFailed {
// If slapd exited before it was killed, print its logs.
t.Logf("%s\n", slapdOut)
logLines, err := ioutil.ReadAll(logs)
if err != nil {
t.Log(string(logLines))
}
}
}
}()
go func() {
wg.Wait()
once.Do(func() { slapdFailed = true })
}()
defer func() {
once.Do(func() { slapdFailed = false })
cmd.Process.Kill()
wg.Wait()
}()
// Try a few times to connect to the LDAP server. On slower machines
// it can take a while for it to come up.
connected := false
wait := 100 * time.Millisecond
for i := 0; i < 5; i++ {
time.Sleep(wait)
ldapadd := exec.Command(
"ldapadd", "-x",
"-D", "cn=admin,dc=example,dc=org",
"-w", "admin",
"-f", schemaPath,
"-H", "ldap://localhost:10389/",
)
if out, err := ldapadd.CombinedOutput(); err != nil {
t.Logf("ldapadd: %s", out)
wait = wait * 2 // backoff
continue
}
connected = true
break
t.Fatal(err)
}
if !connected {
t.Errorf("ldapadd command failed")
return
defer slapd.Terminate(ctx)
ip, err := slapd.Host(ctx)
if err != nil {
t.Fatal(err)
}
port, err := slapd.MappedPort(ctx, "389")
if err != nil {
t.Fatal(err)
}
tlsPort, err := slapd.MappedPort(ctx, "636")
if err != nil {
t.Fatal(err)
}
// Shallow copy.
@@ -858,17 +760,17 @@ func runTests(t *testing.T, schema string, connMethod connectionMethod, config *
// group search configuration.
switch connMethod {
case connectStartTLS:
c.Host = "localhost:10389"
c.RootCA = "testdata/ca.crt"
c.Host = fmt.Sprintf("%s:%s", ip, port.Port())
c.RootCA = "testdata/certs/ca.crt"
c.StartTLS = true
case connectLDAPS:
c.Host = "localhost:10636"
c.RootCA = "testdata/ca.crt"
c.Host = fmt.Sprintf("%s:%s", ip, tlsPort.Port())
c.RootCA = "testdata/certs/ca.crt"
case connectInsecureSkipVerify:
c.Host = "localhost:10636"
c.Host = fmt.Sprintf("%s:%s", ip, tlsPort.Port())
c.InsecureSkipVerify = true
case connectLDAP:
c.Host = "localhost:10389"
c.Host = fmt.Sprintf("%s:%s", ip, port.Port())
c.InsecureNoSSL = true
}
@@ -934,98 +836,3 @@ func runTests(t *testing.T, schema string, connMethod connectionMethod, config *
})
}
}
// Standard OpenLDAP schema files to include.
//
// These are copied from the /etc/openldap/schema directory.
var includeFiles = []string{
"core.schema",
"cosine.schema",
"inetorgperson.schema",
"misc.schema",
"nis.schema",
"openldap.schema",
}
// tmplData is the struct used to execute the SLAPD config template.
type tmplData struct {
// Directory for database to be writen to.
TempDir string
// List of schema files to include.
Includes []string
// TLS assets for LDAPS.
TLSKeyPath string
TLSCertPath string
}
// Config template copied from:
// http://www.zytrax.com/books/ldap/ch5/index.html#step1-slapd
//
// TLS instructions found here:
// http://www.openldap.org/doc/admin24/tls.html
var slapdConfigTmpl = template.Must(template.New("").Parse(`
{{ range $i, $include := .Includes }}
include {{ $include }}
{{ end }}
# MODULELOAD definitions
# not required (comment out) before version 2.3
moduleload back_bdb.la
database bdb
suffix "dc=example,dc=org"
# root or superuser
rootdn "cn=admin,dc=example,dc=org"
rootpw admin
# The database directory MUST exist prior to running slapd AND
# change path as necessary
directory {{ .TempDir }}
TLSCertificateFile {{ .TLSCertPath }}
TLSCertificateKeyFile {{ .TLSKeyPath }}
# Indices to maintain for this directory
# unique id so equality match only
index uid eq
# allows general searching on commonname, givenname and email
index cn,gn,mail eq,sub
# allows multiple variants on surname searching
index sn eq,sub
# sub above includes subintial,subany,subfinal
# optimise department searches
index ou eq
# if searches will include objectClass uncomment following
# index objectClass eq
# shows use of default index parameter
index default eq,sub
# indices missing - uses default eq,sub
index telephonenumber
# other database parameters
# read more in slapd.conf reference section
cachesize 10000
checkpoint 128 15
`))
func tlsAssets(t *testing.T, wd string) (certPath, keyPath string) {
certPath = filepath.Join(wd, "testdata", "server.crt")
keyPath = filepath.Join(wd, "testdata", "server.key")
for _, p := range []string{certPath, keyPath} {
if _, err := os.Stat(p); err != nil {
t.Fatalf("failed to find TLS asset file: %s %v", p, err)
}
}
return
}
func includes(t *testing.T, wd string) (paths []string) {
for _, f := range includeFiles {
p := filepath.Join(wd, "testdata", f)
if _, err := os.Stat(p); err != nil {
t.Fatalf("failed to find schema file: %s %v", p, err)
}
paths = append(paths, p)
}
return
}