mirror of
https://github.com/netauth/ldap.git
synced 2026-11-03 11:43:44 -06:00
An LDAP client only accepts the result that pairs with the request it
sent, and silently ignores anything else. Unrouted operations were
answered with a generic result, which encodes with the wrong tag, so a
search with an unexpected base DN (for example the empty base that
ldapsearch uses when -b is omitted) left the client waiting forever
instead of reporting an error.
Every failure path now terminates the request with the matching result
type, panics included, so a single wrapper covers all operations rather
than one responder per operation. NetAuth calls carry a 30s deadline
because the gRPC client dials lazily and applies none of its own, and
refused operations and failed authentication are logged, which is what
an operator needs when a client appears to hang.
💘 Generated with Crush
Assisted-by: Crush:qwen38_flash_next_mtp
157 lines
4.8 KiB
Go
157 lines
4.8 KiB
Go
package ldap
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/vjeantet/goldap/message"
|
|
ldap "github.com/vjeantet/ldapserver"
|
|
)
|
|
|
|
// recordingWriter captures everything written to it so tests can
|
|
// assert on the exact sequence of responses a handler produces.
|
|
type recordingWriter struct {
|
|
responses []message.ProtocolOp
|
|
}
|
|
|
|
func (w *recordingWriter) Write(po message.ProtocolOp) {
|
|
w.responses = append(w.responses, po)
|
|
}
|
|
|
|
func testLogger() hclog.Logger { return hclog.NewNullLogger() }
|
|
|
|
func TestGuardRecoversPanic(t *testing.T) {
|
|
s := &server{l: testLogger()}
|
|
|
|
h := s.guard(func(w ldap.ResponseWriter, m *ldap.Message) {
|
|
panic("boom")
|
|
})
|
|
|
|
w := &recordingWriter{}
|
|
h(w, mustDecode(t, berSearchRequest("ou=entities,dc=netauth,dc=example,dc=com")))
|
|
|
|
if len(w.responses) != 1 {
|
|
t.Fatalf("got %d responses, want 1", len(w.responses))
|
|
}
|
|
if _, ok := w.responses[0].(message.SearchResultDone); !ok {
|
|
t.Errorf("response is %T, want message.SearchResultDone", w.responses[0])
|
|
}
|
|
if code := resultCode(t, w.responses[0]); code != ldap.LDAPResultOperationsError {
|
|
t.Errorf("result code is %d, want %d", code, ldap.LDAPResultOperationsError)
|
|
}
|
|
}
|
|
|
|
func TestGuardSilentAfterPartialWrite(t *testing.T) {
|
|
s := &server{l: testLogger()}
|
|
|
|
h := s.guard(func(w ldap.ResponseWriter, m *ldap.Message) {
|
|
w.Write(ldap.NewSearchResultEntry("cn=test"))
|
|
panic("boom")
|
|
})
|
|
|
|
w := &recordingWriter{}
|
|
h(w, mustDecode(t, berSearchRequest("ou=entities,dc=netauth,dc=example,dc=com")))
|
|
|
|
if len(w.responses) != 1 {
|
|
t.Errorf("got %d responses, want 1", len(w.responses))
|
|
}
|
|
if _, ok := w.responses[0].(message.SearchResultEntry); !ok {
|
|
t.Errorf("response is %T, want the partial entry only", w.responses[0])
|
|
}
|
|
}
|
|
|
|
func TestGuardPassesThrough(t *testing.T) {
|
|
s := &server{l: testLogger()}
|
|
|
|
h := s.guard(func(w ldap.ResponseWriter, m *ldap.Message) {
|
|
w.Write(ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess))
|
|
})
|
|
|
|
w := &recordingWriter{}
|
|
h(w, &ldap.Message{})
|
|
|
|
if len(w.responses) != 1 {
|
|
t.Fatalf("got %d responses, want 1", len(w.responses))
|
|
}
|
|
if _, ok := w.responses[0].(message.SearchResultDone); !ok {
|
|
t.Errorf("response is %T, want message.SearchResultDone", w.responses[0])
|
|
}
|
|
}
|
|
|
|
// TestResultForRequestPairing is the regression test for the client
|
|
// hang: an LDAP client only accepts the result type that pairs with
|
|
// the request it sent and silently discards anything else, so a
|
|
// mismatched type leaves it waiting for a done message forever.
|
|
func TestResultForRequestPairing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
op message.ProtocolOp
|
|
want string
|
|
}{
|
|
{"search", message.SearchRequest{}, "SearchResultDone"},
|
|
{"bind", message.BindRequest{}, "BindResponse"},
|
|
{"extended", message.ExtendedRequest{}, "ExtendedResponse"},
|
|
{"compare", message.CompareRequest{}, "CompareResponse"},
|
|
{"add", message.AddRequest{}, "AddResponse"},
|
|
{"delete", message.DelRequest("cn=x,dc=example,dc=com"), "DelResponse"},
|
|
{"modify", message.ModifyRequest{}, "ModifyResponse"},
|
|
{"modifyDN", message.ModifyDNRequest{}, "ModifyDNResponse"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
res, ok := resultForRequest(tc.op, ldap.LDAPResultUnwillingToPerform, "nope")
|
|
if !ok {
|
|
t.Fatal("no result produced for a request that needs one")
|
|
}
|
|
if got := reflect.TypeOf(res).Name(); got != tc.want {
|
|
t.Errorf("result type is %s, want %s", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Abandon is never answered, so it must produce nothing rather
|
|
// than an unsolicited result.
|
|
if res, ok := resultForRequest(message.AbandonRequest(1), ldap.LDAPResultSuccess, ""); ok {
|
|
t.Errorf("abandon produced %T, want no response", res)
|
|
}
|
|
}
|
|
|
|
// TestSearchResultDoneWireTag pins the search reply to the
|
|
// searchResDone application tag (5). A reply carrying any other tag,
|
|
// including the bare LDAPResult universal SEQUENCE, is ignored by
|
|
// clients and hangs them.
|
|
func TestSearchResultDoneWireTag(t *testing.T) {
|
|
res, ok := resultForRequest(message.SearchRequest{}, ldap.LDAPResultUnwillingToPerform, "nope")
|
|
if !ok {
|
|
t.Fatal("no result produced for a search request")
|
|
}
|
|
|
|
m := message.NewLDAPMessageWithProtocolOp(res)
|
|
m.SetMessageID(2)
|
|
b, err := m.Write()
|
|
if err != nil {
|
|
t.Fatalf("encoding response: %s", err)
|
|
}
|
|
raw := b.Bytes()
|
|
|
|
// Descend from the LDAPMessage SEQUENCE into the protocolOp.
|
|
off := 0
|
|
if _, off, err = message.ParseTagAndLength(raw, off); err != nil {
|
|
t.Fatalf("message tag: %s", err)
|
|
}
|
|
var id message.TagAndLength
|
|
if id, off, err = message.ParseTagAndLength(raw, off); err != nil {
|
|
t.Fatalf("messageID tag: %s", err)
|
|
}
|
|
off += id.Length
|
|
op, _, err := message.ParseTagAndLength(raw, off)
|
|
if err != nil {
|
|
t.Fatalf("protocolOp tag: %s", err)
|
|
}
|
|
if op.Class != 1 || op.Tag != 5 {
|
|
t.Errorf("protocolOp is class %d tag %d, want APPLICATION (1) tag 5", op.Class, op.Tag)
|
|
}
|
|
}
|