mirror of https://github.com/dexidp/dex.git
2 changed files with 457 additions and 0 deletions
@ -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 |
||||||
|
} |
||||||
Loading…
Reference in new issue