connector/ldap: add LDAP integration tests
This commit is contained in:
parent
943253fece
commit
4a93b55c8b
2 changed files with 457 additions and 0 deletions
|
@ -154,6 +154,10 @@ func (c *Config) OpenConnector(logger logrus.FieldLogger) (interface {
|
|||
connector.PasswordConnector
|
||||
connector.RefreshConnector
|
||||
}, error) {
|
||||
return c.openConnector(logger)
|
||||
}
|
||||
|
||||
func (c *Config) openConnector(logger logrus.FieldLogger) (*ldapConnector, error) {
|
||||
|
||||
requiredFields := []struct {
|
||||
name string
|
||||
|
|
453
connector/ldap/ldap_test.go
Normal file
453
connector/ldap/ldap_test.go
Normal file
|
@ -0,0 +1,453 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
|
||||
"github.com/coreos/dex/connector"
|
||||
)
|
||||
|
||||
const envVar = "DEX_LDAP_TESTS"
|
||||
|
||||
// subtest is a login test against a given schema.
|
||||
type subtest struct {
|
||||
// Name of the sub-test.
|
||||
name string
|
||||
|
||||
// Password credentials, and if the connector should request
|
||||
// groups as well.
|
||||
username string
|
||||
password string
|
||||
groups bool
|
||||
|
||||
// Expected result of the login.
|
||||
wantErr bool
|
||||
wantBadPW bool
|
||||
want connector.Identity
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
dn: cn=jane,ou=People,dc=example,dc=org
|
||||
objectClass: person
|
||||
objectClass: iNetOrgPerson
|
||||
sn: doe
|
||||
cn: jane
|
||||
mail: janedoe@example.com
|
||||
userpassword: foo
|
||||
|
||||
dn: cn=john,ou=People,dc=example,dc=org
|
||||
objectClass: person
|
||||
objectClass: iNetOrgPerson
|
||||
sn: doe
|
||||
cn: john
|
||||
mail: johndoe@example.com
|
||||
userpassword: bar
|
||||
`
|
||||
c := &Config{}
|
||||
c.UserSearch.BaseDN = "ou=People,dc=example,dc=org"
|
||||
c.UserSearch.NameAttr = "cn"
|
||||
c.UserSearch.EmailAttr = "mail"
|
||||
c.UserSearch.IDAttr = "DN"
|
||||
c.UserSearch.Username = "cn"
|
||||
|
||||
tests := []subtest{
|
||||
{
|
||||
name: "validpassword",
|
||||
username: "jane",
|
||||
password: "foo",
|
||||
want: connector.Identity{
|
||||
UserID: "cn=jane,ou=People,dc=example,dc=org",
|
||||
Username: "jane",
|
||||
Email: "janedoe@example.com",
|
||||
EmailVerified: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validpassword2",
|
||||
username: "john",
|
||||
password: "bar",
|
||||
want: connector.Identity{
|
||||
UserID: "cn=john,ou=People,dc=example,dc=org",
|
||||
Username: "john",
|
||||
Email: "johndoe@example.com",
|
||||
EmailVerified: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalidpassword",
|
||||
username: "jane",
|
||||
password: "badpassword",
|
||||
wantBadPW: true,
|
||||
},
|
||||
{
|
||||
name: "invaliduser",
|
||||
username: "idontexist",
|
||||
password: "foo",
|
||||
wantBadPW: true, // Want invalid password, not a query error.
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, schema, c, tests)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
dn: cn=jane,ou=People,dc=example,dc=org
|
||||
objectClass: person
|
||||
objectClass: iNetOrgPerson
|
||||
sn: doe
|
||||
cn: jane
|
||||
mail: janedoe@example.com
|
||||
userpassword: foo
|
||||
|
||||
dn: cn=john,ou=People,dc=example,dc=org
|
||||
objectClass: person
|
||||
objectClass: iNetOrgPerson
|
||||
sn: doe
|
||||
cn: john
|
||||
mail: johndoe@example.com
|
||||
userpassword: bar
|
||||
|
||||
# Group definitions.
|
||||
|
||||
dn: ou=Groups,dc=example,dc=org
|
||||
objectClass: organizationalUnit
|
||||
ou: Groups
|
||||
|
||||
dn: cn=admins,ou=Groups,dc=example,dc=org
|
||||
objectClass: groupOfNames
|
||||
cn: admins
|
||||
member: cn=john,ou=People,dc=example,dc=org
|
||||
member: cn=jane,ou=People,dc=example,dc=org
|
||||
|
||||
dn: cn=developers,ou=Groups,dc=example,dc=org
|
||||
objectClass: groupOfNames
|
||||
cn: developers
|
||||
member: cn=jane,ou=People,dc=example,dc=org
|
||||
`
|
||||
c := &Config{}
|
||||
c.UserSearch.BaseDN = "ou=People,dc=example,dc=org"
|
||||
c.UserSearch.NameAttr = "cn"
|
||||
c.UserSearch.EmailAttr = "mail"
|
||||
c.UserSearch.IDAttr = "DN"
|
||||
c.UserSearch.Username = "cn"
|
||||
c.GroupSearch.BaseDN = "ou=Groups,dc=example,dc=org"
|
||||
c.GroupSearch.UserAttr = "DN"
|
||||
c.GroupSearch.GroupAttr = "member"
|
||||
c.GroupSearch.NameAttr = "cn"
|
||||
|
||||
tests := []subtest{
|
||||
{
|
||||
name: "validpassword",
|
||||
username: "jane",
|
||||
password: "foo",
|
||||
groups: true,
|
||||
want: connector.Identity{
|
||||
UserID: "cn=jane,ou=People,dc=example,dc=org",
|
||||
Username: "jane",
|
||||
Email: "janedoe@example.com",
|
||||
EmailVerified: true,
|
||||
Groups: []string{"admins", "developers"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validpassword2",
|
||||
username: "john",
|
||||
password: "bar",
|
||||
groups: true,
|
||||
want: connector.Identity{
|
||||
UserID: "cn=john,ou=People,dc=example,dc=org",
|
||||
Username: "john",
|
||||
Email: "johndoe@example.com",
|
||||
EmailVerified: true,
|
||||
Groups: []string{"admins"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTests(t, schema, c, tests)
|
||||
}
|
||||
|
||||
// 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 DEX_LDAP_TESTS must be set to "1"
|
||||
func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
|
||||
if os.Getenv(envVar) != "1" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
tempDir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
configBytes := new(bytes.Buffer)
|
||||
|
||||
if err := slapdConfigTmpl.Execute(configBytes, tmplData{tempDir, includes(t)}); 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)
|
||||
}
|
||||
|
||||
socketPath := url.QueryEscape(filepath.Join(tempDir, "ldap.unix"))
|
||||
|
||||
slapdOut := new(bytes.Buffer)
|
||||
|
||||
cmd := exec.Command(
|
||||
"slapd",
|
||||
"-d", "any",
|
||||
"-h", "ldap://localhost:10363/ 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)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
once.Do(func() { slapdFailed = true })
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
once.Do(func() { slapdFailed = false })
|
||||
cmd.Process.Kill()
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
// Wait for slapd to come up.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
ldapadd := exec.Command(
|
||||
"ldapadd", "-x",
|
||||
"-D", "cn=admin,dc=example,dc=org",
|
||||
"-w", "admin",
|
||||
"-f", schemaPath,
|
||||
"-H", "ldap://localhost:10363/",
|
||||
)
|
||||
if out, err := ldapadd.CombinedOutput(); err != nil {
|
||||
t.Errorf("ldapadd: %s", out)
|
||||
return
|
||||
}
|
||||
|
||||
// Shallow copy.
|
||||
c := *config
|
||||
|
||||
// We need to configure host parameters but don't want to overwrite user or
|
||||
// group search configuration.
|
||||
c.Host = "localhost:10363"
|
||||
c.InsecureNoSSL = true
|
||||
c.BindDN = "cn=admin,dc=example,dc=org"
|
||||
c.BindPW = "admin"
|
||||
|
||||
l := &logrus.Logger{Out: ioutil.Discard, Formatter: &logrus.TextFormatter{}}
|
||||
|
||||
conn, err := c.openConnector(l)
|
||||
if err != nil {
|
||||
t.Errorf("open connector: %v", err)
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if test.name == "" {
|
||||
t.Fatal("go a subtest with no name")
|
||||
}
|
||||
|
||||
// Run the subtest.
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := connector.Scopes{OfflineAccess: true, Groups: test.groups}
|
||||
ident, validPW, err := conn.Login(context.Background(), s, test.username, test.password)
|
||||
if err != nil {
|
||||
if !test.wantErr {
|
||||
t.Fatalf("query failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if test.wantErr {
|
||||
t.Fatalf("wanted query to fail")
|
||||
}
|
||||
|
||||
if !validPW {
|
||||
if !test.wantBadPW {
|
||||
t.Fatalf("invalid password: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if test.wantBadPW {
|
||||
t.Fatalf("wanted invalid password")
|
||||
}
|
||||
got := ident
|
||||
got.ConnectorData = nil
|
||||
|
||||
if diff := pretty.Compare(test.want, got); diff != "" {
|
||||
t.Error(diff)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that refresh tokens work.
|
||||
ident, err = conn.Refresh(context.Background(), s, ident)
|
||||
if err != nil {
|
||||
t.Errorf("refresh failed: %v", err)
|
||||
}
|
||||
|
||||
got = ident
|
||||
got.ConnectorData = nil
|
||||
|
||||
if diff := pretty.Compare(test.want, got); diff != "" {
|
||||
t.Errorf("after refresh: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Config template copied from:
|
||||
// http://www.zytrax.com/books/ldap/ch5/index.html#step1-slapd
|
||||
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 }}
|
||||
|
||||
# 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 includes(t *testing.T) (paths []string) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("getting working directory: %v", err)
|
||||
}
|
||||
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
|
||||
}
|
Reference in a new issue