diff --git a/domain/auth/ldap/ad_test.go b/domain/auth/ldap/ad_test.go index 401b7219..0f9c7a0c 100644 --- a/domain/auth/ldap/ad_test.go +++ b/domain/auth/ldap/ad_test.go @@ -20,7 +20,7 @@ import ( ld "gopkg.in/ldap.v2" ) -// Works against AD server in Azure confgiured using: +// Works against AD server in Azure configured using: // // https://auth0.com/docs/connector/test-dc // @@ -45,53 +45,24 @@ var testConfigPublicAD = lm.LDAPConfig{ AttributeGroupMember: "member", } -func TestADServer_UserList(t *testing.T) { +func TestUserFilter_PublicAD(t *testing.T) { testConfigPublicAD.UserFilter = "(|(objectCategory=person)(objectClass=user)(objectClass=inetOrgPerson))" - testConfigPublicAD.GroupFilter = "" - userAttrs := testConfigPublicAD.GetUserFilterAttributes() - l, err := Connect(testConfigPublicAD) + e, err := executeUserFilter(testConfigPublicAD) if err != nil { - t.Error("Error: unable to dial LDAP server: ", err.Error()) + t.Error("unable to exeucte user filter", err.Error()) return } - defer l.Close() - - // Authenticate with AD server using admin credentials. - t.Log("Binding LDAP admin user") - err = l.Bind(testConfigPublicAD.BindDN, testConfigPublicAD.BindPassword) - if err != nil { - t.Error("Error: unable to bind specified admin user to AD: ", err.Error()) + if len(e) == 0 { + t.Error("Received ZERO LDAP search entries") return } - searchRequest := ld.NewSearchRequest( - testConfigPublicAD.BaseDN, - ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, - testConfigPublicAD.UserFilter, - userAttrs, - nil, - ) + t.Logf("LDAP search entries found: %d", len(e)) - sr, err := l.Search(searchRequest) - if err != nil { - t.Error("Error: unable to execute directory search: ", err.Error()) - return - } - - t.Logf("AD search entries found: %d", len(sr.Entries)) - if len(sr.Entries) == 0 { - t.Error("Received ZERO AD search entries") - return - } - - for _, entry := range sr.Entries { + for _, u := range e { t.Logf("[%s] %s (%s %s) @ %s\n", - entry.GetAttributeValue(testConfigPublicAD.AttributeUserRDN), - entry.GetAttributeValue("cn"), - entry.GetAttributeValue(testConfigPublicAD.AttributeUserFirstname), - entry.GetAttributeValue(testConfigPublicAD.AttributeUserLastname), - entry.GetAttributeValue(testConfigPublicAD.AttributeUserEmail)) + u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email) } } @@ -101,7 +72,7 @@ func TestADServer_Groups(t *testing.T) { groupAttrs := testConfigPublicAD.GetGroupFilterAttributes() userAttrs := testConfigPublicAD.GetUserFilterAttributes() - l, err := Connect(testConfigPublicAD) + l, err := connect(testConfigPublicAD) if err != nil { t.Error("Error: unable to dial AD server: ", err.Error()) return @@ -142,8 +113,6 @@ func TestADServer_Groups(t *testing.T) { t.Log("Found group", group.DN) rawMembers := group.GetAttributeValues(testConfigPublicAD.AttributeGroupMember) - fmt.Printf("%s", group.DN) - if len(rawMembers) == 0 { t.Log("Error: group member attribute returned no users") continue @@ -187,61 +156,43 @@ func TestADServer_Groups(t *testing.T) { } } } -func TestADServer_Authenticate(t *testing.T) { - testConfigPublicAD.UserFilter = "" - testConfigPublicAD.GroupFilter = "" - userAttrs := testConfigPublicAD.GetUserFilterAttributes() - l, err := Connect(testConfigPublicAD) +func TestAuthenticate_PublicAD(t *testing.T) { + l, err := connect(testConfigPublicAD) if err != nil { t.Error("Error: unable to dial LDAP server: ", err.Error()) return } defer l.Close() - // Authenticate with AD server using admin credentials. - t.Log("Binding AD admin user") - err = l.Bind(testConfigPublicAD.BindDN, testConfigPublicAD.BindPassword) + ok, err := authenticate(l, testConfigPublicAD, "bob.johnson", "Pass@word1!") if err != nil { - t.Error("Error: unable to bind specified admin user to AD: ", err.Error()) + t.Error("error during LDAP authentication: ", err.Error()) return } - username := `bob.johnson` - password := "Pass@word1!" - filter := fmt.Sprintf("(%s=%s)", testConfigPublicAD.AttributeUserRDN, username) - - searchRequest := ld.NewSearchRequest( - testConfigPublicAD.BaseDN, - ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, - filter, - userAttrs, - nil, - ) - - t.Log("AD search filter:", filter) - sr, err := l.Search(searchRequest) - if err != nil { - t.Error("Error: unable to execute directory search: ", err.Error()) - return + if !ok { + t.Error("failed LDAP authentication") } - if len(sr.Entries) == 0 { - t.Error("Error: user not found") - return - } - if len(sr.Entries) != 1 { - t.Error("Error: too many users found during authentication") - return - } - - userdn := sr.Entries[0].DN - - // Bind as the user to verify their password - err = l.Bind(userdn, password) - if err != nil { - t.Error("Error: invalid credentials", err.Error()) - return - } - - t.Log("Authenticated", username) + t.Log("Authenticated") +} + +func TestNotAuthenticate_PublicAD(t *testing.T) { + l, err := connect(testConfigPublicAD) + if err != nil { + t.Error("Error: unable to dial LDAP server: ", err.Error()) + return + } + defer l.Close() + + ok, err := authenticate(l, testConfigPublicAD, "junk", "junk") + if err != nil { + t.Error("error during LDAP authentication: ", err.Error()) + return + } + if ok { + t.Error("incorrect LDAP authentication") + } + + t.Log("Not authenticated") } diff --git a/domain/auth/ldap/ldap.go b/domain/auth/ldap/ldap.go index fe9211ec..879dae9c 100644 --- a/domain/auth/ldap/ldap.go +++ b/domain/auth/ldap/ldap.go @@ -16,12 +16,13 @@ import ( "fmt" lm "github.com/documize/community/model/auth" + // "github.com/documize/community/model/user" "github.com/pkg/errors" ld "gopkg.in/ldap.v2" ) // Connect establishes connection to LDAP server. -func Connect(c lm.LDAPConfig) (l *ld.Conn, err error) { +func connect(c lm.LDAPConfig) (l *ld.Conn, err error) { address := fmt.Sprintf("%s:%d", c.ServerHost, c.ServerPort) fmt.Println("Connecting to LDAP server", address) @@ -43,3 +44,97 @@ func Connect(c lm.LDAPConfig) (l *ld.Conn, err error) { return } + +// Authenticate user against LDAP provider. +func authenticate(l *ld.Conn, c lm.LDAPConfig, username, pwd string) (success bool, err error) { + success = false + userAttrs := c.GetUserFilterAttributes() + filter := fmt.Sprintf("(%s=%s)", c.AttributeUserRDN, username) + + searchRequest := ld.NewSearchRequest( + c.BaseDN, + ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, + filter, + userAttrs, + nil, + ) + + err = l.Bind(c.BindDN, c.BindPassword) + if err != nil { + errors.Wrap(err, "unable to bind admin user") + return + } + + sr, err := l.Search(searchRequest) + if err != nil { + err = errors.Wrap(err, "unable to execute LDAP search during authentication") + return + } + + if len(sr.Entries) == 0 { + err = errors.Wrap(err, "user not found during LDAP authentication") + return + } + if len(sr.Entries) != 1 { + err = errors.Wrap(err, "dupe users found during LDAP authentication") + return + } + + userdn := sr.Entries[0].DN + + // Bind as the user to verify their password + err = l.Bind(userdn, pwd) + if err != nil { + return false, nil + } + + return true, nil +} + +// ExecuteUserFilter returns all matching LDAP users. +func executeUserFilter(c lm.LDAPConfig) (u []lm.LDAPUser, err error) { + l, err := connect(c) + if err != nil { + err = errors.Wrap(err, "unable to dial LDAP server") + return + } + defer l.Close() + + // Authenticate with LDAP server using admin credentials. + err = l.Bind(c.BindDN, c.BindPassword) + if err != nil { + errors.Wrap(err, "unable to bind admin user") + return + } + + searchRequest := ld.NewSearchRequest( + c.BaseDN, + ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, + c.UserFilter, + c.GetUserFilterAttributes(), + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + errors.Wrap(err, "unable to execute directory search for user filter "+c.UserFilter) + return + } + + for _, e := range sr.Entries { + u = append(u, extractUser(c, e)) + } + + return +} + +// extractUser build user record from LDAP result attributes. +func extractUser(c lm.LDAPConfig, e *ld.Entry) (u lm.LDAPUser) { + u.Firstname = e.GetAttributeValue(c.AttributeUserFirstname) + u.Lastname = e.GetAttributeValue(c.AttributeUserLastname) + u.Email = e.GetAttributeValue(c.AttributeUserEmail) + u.RemoteID = e.GetAttributeValue(c.AttributeUserRDN) + u.CN = e.GetAttributeValue("cn") + + return +} diff --git a/domain/auth/ldap/local_test.go b/domain/auth/ldap/local_test.go index af4fcbed..eb16f73f 100644 --- a/domain/auth/ldap/local_test.go +++ b/domain/auth/ldap/local_test.go @@ -42,54 +42,24 @@ var testConfigLocalLDAP = lm.LDAPConfig{ AttributeGroupMember: "member", } -func TestLocalLDAPServer_AllUsers(t *testing.T) { +func TestUserFilter_LocalLDAP(t *testing.T) { testConfigLocalLDAP.UserFilter = "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" - testConfigLocalLDAP.GroupFilter = "" - userAttrs := testConfigLocalLDAP.GetUserFilterAttributes() - l, err := Connect(testConfigLocalLDAP) + e, err := executeUserFilter(testConfigLocalLDAP) if err != nil { - t.Error("Error: unable to dial LDAP server: ", err.Error()) + t.Error("unable to exeucte user filter", err.Error()) return } - defer l.Close() - - // Authenticate with LDAP server using admin credentials. - t.Log("Binding LDAP admin user") - err = l.Bind(testConfigLocalLDAP.BindDN, testConfigLocalLDAP.BindPassword) - if err != nil { - t.Error("Error: unable to bind specified admin user to LDAP: ", err.Error()) - return - } - - searchRequest := ld.NewSearchRequest( - testConfigLocalLDAP.BaseDN, - ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, - testConfigLocalLDAP.UserFilter, - userAttrs, - nil, - ) - - t.Log("LDAP search filter:", testConfigLocalLDAP.UserFilter) - sr, err := l.Search(searchRequest) - if err != nil { - t.Error("Error: unable to execute directory search: ", err.Error()) - return - } - - t.Logf("LDAP search entries found: %d", len(sr.Entries)) - if len(sr.Entries) == 0 { + if len(e) == 0 { t.Error("Received ZERO LDAP search entries") return } - for _, entry := range sr.Entries { + t.Logf("LDAP search entries found: %d", len(e)) + + for _, u := range e { t.Logf("[%s] %s (%s %s) @ %s\n", - entry.GetAttributeValue(testConfigLocalLDAP.AttributeUserRDN), - entry.GetAttributeValue("cn"), - entry.GetAttributeValue(testConfigLocalLDAP.AttributeUserFirstname), - entry.GetAttributeValue(testConfigLocalLDAP.AttributeUserLastname), - entry.GetAttributeValue(testConfigLocalLDAP.AttributeUserEmail)) + u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email) } } @@ -99,7 +69,7 @@ func TestLocalLDAPServer_UsersInGroup(t *testing.T) { groupAttrs := testConfigLocalLDAP.GetGroupFilterAttributes() userAttrs := testConfigLocalLDAP.GetUserFilterAttributes() - l, err := Connect(testConfigLocalLDAP) + l, err := connect(testConfigLocalLDAP) if err != nil { t.Error("Error: unable to dial LDAP server: ", err.Error()) return @@ -183,62 +153,43 @@ func TestLocalLDAPServer_UsersInGroup(t *testing.T) { } } } -func TestLocalLDAP_Authenticate(t *testing.T) { - testConfigLocalLDAP.UserFilter = "" - testConfigLocalLDAP.GroupFilter = "" - userAttrs := testConfigLocalLDAP.GetUserFilterAttributes() - l, err := Connect(testConfigLocalLDAP) +func TestAuthenticate_LocalLDAP(t *testing.T) { + l, err := connect(testConfigLocalLDAP) if err != nil { t.Error("Error: unable to dial LDAP server: ", err.Error()) return } defer l.Close() - // Authenticate with LDAP server using admin credentials. - t.Log("Binding LDAP admin user") - err = l.Bind(testConfigLocalLDAP.BindDN, testConfigLocalLDAP.BindPassword) + ok, err := authenticate(l, testConfigLocalLDAP, "professor", "professor") if err != nil { - t.Error("Error: unable to bind specified admin user to LDAP: ", err.Error()) + t.Error("error during LDAP authentication: ", err.Error()) return } - - username := "professor" - password := "professor" - filter := fmt.Sprintf("(%s=%s)", testConfigPublicLDAP.AttributeUserRDN, username) - - searchRequest := ld.NewSearchRequest( - testConfigLocalLDAP.BaseDN, - ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, - filter, - userAttrs, - nil, - ) - - t.Log("LDAP search filter:", filter) - sr, err := l.Search(searchRequest) - if err != nil { - t.Error("Error: unable to execute directory search: ", err.Error()) - return + if !ok { + t.Error("failed LDAP authentication") } - if len(sr.Entries) == 0 { - t.Error("Error: user not found") - return - } - if len(sr.Entries) != 1 { - t.Error("Error: too many users found during authentication") - return - } - - userdn := sr.Entries[0].DN - - // Bind as the user to verify their password - err = l.Bind(userdn, password) - if err != nil { - t.Error("Error: invalid credentials", err.Error()) - return - } - - t.Log("Authenticated", username) + t.Log("Authenticated") +} + +func TestNotAuthenticate_LocalLDAP(t *testing.T) { + l, err := connect(testConfigLocalLDAP) + if err != nil { + t.Error("Error: unable to dial LDAP server: ", err.Error()) + return + } + defer l.Close() + + ok, err := authenticate(l, testConfigLocalLDAP, "junk", "junk") + if err != nil { + t.Error("error during LDAP authentication: ", err.Error()) + return + } + if ok { + t.Error("incorrect LDAP authentication") + } + + t.Log("Not authenticated") } diff --git a/domain/auth/ldap/public_test.go b/domain/auth/ldap/public_test.go index eb1eb6fe..c48d09be 100644 --- a/domain/auth/ldap/public_test.go +++ b/domain/auth/ldap/public_test.go @@ -41,53 +41,24 @@ var testConfigPublicLDAP = lm.LDAPConfig{ AttributeGroupMember: "uniqueMember", } -func TestPublicLDAPServer_UserList(t *testing.T) { +func TestUserFilter_PublicLDAP(t *testing.T) { testConfigPublicLDAP.UserFilter = "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" - testConfigPublicLDAP.GroupFilter = "" - userAttrs := testConfigPublicLDAP.GetUserFilterAttributes() - l, err := Connect(testConfigPublicLDAP) + e, err := executeUserFilter(testConfigPublicLDAP) if err != nil { - t.Error("Error: unable to dial LDAP server: ", err.Error()) + t.Error("unable to exeucte user filter", err.Error()) return } - defer l.Close() - - // Authenticate with LDAP server using admin credentials. - t.Log("Binding LDAP admin user") - err = l.Bind(testConfigPublicLDAP.BindDN, testConfigPublicLDAP.BindPassword) - if err != nil { - t.Error("Error: unable to bind specified admin user to LDAP: ", err.Error()) - return - } - - searchRequest := ld.NewSearchRequest( - testConfigPublicLDAP.BaseDN, - ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, - testConfigPublicLDAP.UserFilter, - userAttrs, - nil, - ) - - sr, err := l.Search(searchRequest) - if err != nil { - t.Error("Error: unable to execute directory search: ", err.Error()) - return - } - - t.Logf("LDAP search entries found: %d", len(sr.Entries)) - if len(sr.Entries) == 0 { + if len(e) == 0 { t.Error("Received ZERO LDAP search entries") return } - for _, entry := range sr.Entries { + t.Logf("LDAP search entries found: %d", len(e)) + + for _, u := range e { t.Logf("[%s] %s (%s %s) @ %s\n", - entry.GetAttributeValue(testConfigPublicLDAP.AttributeUserRDN), - entry.GetAttributeValue("cn"), - entry.GetAttributeValue(testConfigPublicLDAP.AttributeUserFirstname), - entry.GetAttributeValue(testConfigPublicLDAP.AttributeUserLastname), - entry.GetAttributeValue(testConfigPublicLDAP.AttributeUserEmail)) + u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email) } } @@ -97,7 +68,7 @@ func TestPublicLDAPServer_Groups(t *testing.T) { groupAttrs := testConfigPublicLDAP.GetGroupFilterAttributes() userAttrs := testConfigPublicLDAP.GetUserFilterAttributes() - l, err := Connect(testConfigPublicLDAP) + l, err := connect(testConfigPublicLDAP) if err != nil { t.Error("Error: unable to dial LDAP server: ", err.Error()) return @@ -182,62 +153,40 @@ func TestPublicLDAPServer_Groups(t *testing.T) { } } -func TestPublicLDAP_Authenticate(t *testing.T) { - testConfigPublicLDAP.UserFilter = "" - testConfigPublicLDAP.GroupFilter = "" - userAttrs := testConfigPublicLDAP.GetUserFilterAttributes() - - l, err := Connect(testConfigPublicLDAP) +func TestAuthenticate_PublicLDAP(t *testing.T) { + l, err := connect(testConfigPublicLDAP) if err != nil { t.Error("Error: unable to dial LDAP server: ", err.Error()) return } defer l.Close() - // Authenticate with LDAP server using admin credentials. - t.Log("Binding LDAP admin user") - err = l.Bind(testConfigPublicLDAP.BindDN, testConfigPublicLDAP.BindPassword) + ok, err := authenticate(l, testConfigPublicLDAP, "newton", "password") if err != nil { - t.Error("Error: unable to bind specified admin user to LDAP: ", err.Error()) + t.Error("error during LDAP authentication: ", err.Error()) return } - - username := "newton" - password := "password" - filter := fmt.Sprintf("(%s=%s)", testConfigPublicLDAP.AttributeUserRDN, username) - - searchRequest := ld.NewSearchRequest( - testConfigPublicLDAP.BaseDN, - ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, - filter, - userAttrs, - nil, - ) - - t.Log("LDAP search filter:", filter) - sr, err := l.Search(searchRequest) - if err != nil { - t.Error("Error: unable to execute directory search: ", err.Error()) - return + if !ok { + t.Error("failed LDAP authentication") } - - if len(sr.Entries) == 0 { - t.Error("Error: user not found") - return - } - if len(sr.Entries) != 1 { - t.Error("Error: too many users found during authentication") - return - } - - userdn := sr.Entries[0].DN - - // Bind as the user to verify their password - err = l.Bind(userdn, password) - if err != nil { - t.Error("Error: invalid credentials", err.Error()) - return - } - - t.Log("Authenticated", username) +} + +func TestNotAuthenticate_PublicLDAP(t *testing.T) { + l, err := connect(testConfigPublicLDAP) + if err != nil { + t.Error("Error: unable to dial LDAP server: ", err.Error()) + return + } + defer l.Close() + + ok, err := authenticate(l, testConfigPublicLDAP, "junk", "junk") + if err != nil { + t.Error("error during LDAP authentication: ", err.Error()) + return + } + if ok { + t.Error("incorrect LDAP authentication") + } + + t.Log("Not authenticated") } diff --git a/model/auth/ldap.go b/model/auth/ldap.go index b9fe5e60..077996b0 100644 --- a/model/auth/ldap.go +++ b/model/auth/ldap.go @@ -146,14 +146,13 @@ func (c *LDAPConfig) GetGroupFilterAttributes() []string { } // LDAPUser details user record returned by LDAP -// type LDAPUser struct { -// ID string `json:"id"` -// Username string `json:"username"` -// Email string `json:"email"` -// Firstname string `json:"firstName"` -// Lastname string `json:"lastName"` -// Enabled bool `json:"enabled"` -// } +type LDAPUser struct { + RemoteID string `json:"remoteId"` + CN string `json:"cn"` + Email string `json:"email"` + Firstname string `json:"firstName"` + Lastname string `json:"lastName"` +} // LDAPAuthRequest data received via LDAP client library // type LDAPAuthRequest struct {