diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 7b41c333a..98e772ba8 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -109,6 +109,14 @@ "Comment": "v2.0.1-6-g44cb478", "Rev": "44cb4788b2ec3c3d158dd3d1b50aba7d66f4b59a" }, + { + "ImportPath": "github.com/vanackere/asn1-ber", + "Rev": "295c7b21db5d9525ad959e3382610f3aff029663" + }, + { + "ImportPath": "github.com/vanackere/ldap", + "Rev": "e29b797d1abde6567ccb4ab56236e033cabf845a" + }, { "ImportPath": "github.com/vaughan0/go-ini", "Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1" diff --git a/Godeps/_workspace/src/github.com/vanackere/asn1-ber/.travis.yml b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/.travis.yml new file mode 100644 index 000000000..8d7d11f32 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/.travis.yml @@ -0,0 +1,12 @@ +language: go +go: + - 1.2 + - 1.3 + - tip +install: + - go list -f '{{range .Imports}}{{.}} {{end}}' ./... | xargs go get -v + - go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs go get -v + - go get code.google.com/p/go.tools/cmd/cover + - go build -v ./... +script: + - go test -v -cover ./... diff --git a/Godeps/_workspace/src/github.com/vanackere/asn1-ber/LICENSE b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/LICENSE new file mode 100644 index 000000000..744875676 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/github.com/vanackere/asn1-ber/README.md b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/README.md new file mode 100644 index 000000000..57ac47fab --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/README.md @@ -0,0 +1,18 @@ +[![GoDoc](https://godoc.org/github.com/vanackere/asn1-ber?status.svg)](https://godoc.org/github.com/vanackere/asn1-ber) [![Build Status](https://travis-ci.org/vanackere/asn1-ber.svg)](https://travis-ci.org/vanackere/asn1-ber) + + +ASN1 BER Encoding / Decoding Library for the GO programming language. +--------------------------------------------------------------------- + +Required libraries: + None + +Working: + Very basic encoding / decoding needed for LDAP protocol + +Tests Implemented: + A few + +TODO: + Fix all encoding / decoding to conform to ASN1 BER spec + Implement Tests / Benchmarks diff --git a/Godeps/_workspace/src/github.com/vanackere/asn1-ber/ber.go b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/ber.go new file mode 100644 index 000000000..0eb821911 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/ber.go @@ -0,0 +1,528 @@ +package ber + +import ( + "bytes" + "fmt" + "io" + "os" + "reflect" +) + +type Packet struct { + ClassType Class + TagType Type + Tag Tag + Value interface{} + ByteValue []byte + Data *bytes.Buffer + Children []*Packet + Description string +} + +type Tag uint8 + +const ( + TagEOC Tag = 0x00 + TagBoolean Tag = 0x01 + TagInteger Tag = 0x02 + TagBitString Tag = 0x03 + TagOctetString Tag = 0x04 + TagNULL Tag = 0x05 + TagObjectIdentifier Tag = 0x06 + TagObjectDescriptor Tag = 0x07 + TagExternal Tag = 0x08 + TagRealFloat Tag = 0x09 + TagEnumerated Tag = 0x0a + TagEmbeddedPDV Tag = 0x0b + TagUTF8String Tag = 0x0c + TagRelativeOID Tag = 0x0d + TagSequence Tag = 0x10 + TagSet Tag = 0x11 + TagNumericString Tag = 0x12 + TagPrintableString Tag = 0x13 + TagT61String Tag = 0x14 + TagVideotexString Tag = 0x15 + TagIA5String Tag = 0x16 + TagUTCTime Tag = 0x17 + TagGeneralizedTime Tag = 0x18 + TagGraphicString Tag = 0x19 + TagVisibleString Tag = 0x1a + TagGeneralString Tag = 0x1b + TagUniversalString Tag = 0x1c + TagCharacterString Tag = 0x1d + TagBMPString Tag = 0x1e + TagBitmask Tag = 0x1f // xxx11111b +) + +var tagMap = map[Tag]string{ + TagEOC: "EOC (End-of-Content)", + TagBoolean: "Boolean", + TagInteger: "Integer", + TagBitString: "Bit String", + TagOctetString: "Octet String", + TagNULL: "NULL", + TagObjectIdentifier: "Object Identifier", + TagObjectDescriptor: "Object Descriptor", + TagExternal: "External", + TagRealFloat: "Real (float)", + TagEnumerated: "Enumerated", + TagEmbeddedPDV: "Embedded PDV", + TagUTF8String: "UTF8 String", + TagRelativeOID: "Relative-OID", + TagSequence: "Sequence and Sequence of", + TagSet: "Set and Set OF", + TagNumericString: "Numeric String", + TagPrintableString: "Printable String", + TagT61String: "T61 String", + TagVideotexString: "Videotex String", + TagIA5String: "IA5 String", + TagUTCTime: "UTC Time", + TagGeneralizedTime: "Generalized Time", + TagGraphicString: "Graphic String", + TagVisibleString: "Visible String", + TagGeneralString: "General String", + TagUniversalString: "Universal String", + TagCharacterString: "Character String", + TagBMPString: "BMP String", +} + +type Class uint8 + +const ( + ClassUniversal Class = 0 // 00xxxxxxb + ClassApplication Class = 64 // 01xxxxxxb + ClassContext Class = 128 // 10xxxxxxb + ClassPrivate Class = 192 // 11xxxxxxb + ClassBitmask Class = 192 // 11xxxxxxb +) + +var ClassMap = map[Class]string{ + ClassUniversal: "Universal", + ClassApplication: "Application", + ClassContext: "Context", + ClassPrivate: "Private", +} + +type Type uint8 + +const ( + TypePrimitive Type = 0 // xx0xxxxxb + TypeConstructed Type = 32 // xx1xxxxxb + TypeBitmask Type = 32 // xx1xxxxxb +) + +var TypeMap = map[Type]string{ + TypePrimitive: "Primitive", + TypeConstructed: "Constructed", +} + +var Debug bool = false + +func PrintBytes(out io.Writer, buf []byte, indent string) { + data_lines := make([]string, (len(buf)/30)+1) + num_lines := make([]string, (len(buf)/30)+1) + + for i, b := range buf { + data_lines[i/30] += fmt.Sprintf("%02x ", b) + num_lines[i/30] += fmt.Sprintf("%02d ", (i+1)%100) + } + + for i := 0; i < len(data_lines); i++ { + out.Write([]byte(indent + data_lines[i] + "\n")) + out.Write([]byte(indent + num_lines[i] + "\n\n")) + } +} + +func PrintPacket(p *Packet) { + printPacket(os.Stdout, p, 0, false) +} + +func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) { + indent_str := "" + + for len(indent_str) != indent { + indent_str += " " + } + + class_str := ClassMap[p.ClassType] + + tagtype_str := TypeMap[p.TagType] + + tag_str := fmt.Sprintf("0x%02X", p.Tag) + + if p.ClassType == ClassUniversal { + tag_str = tagMap[p.Tag] + } + + value := fmt.Sprint(p.Value) + description := "" + + if p.Description != "" { + description = p.Description + ": " + } + + fmt.Fprintf(out, "%s%s(%s, %s, %s) Len=%d %q\n", indent_str, description, class_str, tagtype_str, tag_str, p.Data.Len(), value) + + if printBytes { + PrintBytes(out, p.Bytes(), indent_str) + } + + for _, child := range p.Children { + printPacket(out, child, indent+1, printBytes) + } +} + +func resizeBuffer(in []byte, new_size int) (out []byte) { + out = make([]byte, new_size) + + copy(out, in) + + return +} + +func ReadPacket(reader io.Reader) (*Packet, error) { + var header [2]byte + buf := header[:] + _, err := io.ReadFull(reader, buf) + + if err != nil { + return nil, err + } + + idx := 2 + var datalen int + l := buf[1] + + if l&0x80 == 0 { + // The length is encoded in the bottom 7 bits. + datalen = int(l & 0x7f) + if Debug { + fmt.Printf("Read: datalen = %d len(buf) = %d\n ", l, len(buf)) + + for _, b := range buf { + fmt.Printf("%02X ", b) + } + + fmt.Printf("\n") + } + } else { + // Bottom 7 bits give the number of length bytes to follow. + numBytes := int(l & 0x7f) + if numBytes == 0 { + return nil, fmt.Errorf("invalid length found") + } + idx += numBytes + buf = resizeBuffer(buf, 2+numBytes) + _, err := io.ReadFull(reader, buf[2:]) + + if err != nil { + return nil, err + } + datalen = 0 + for i := 0; i < numBytes; i++ { + b := buf[2+i] + datalen <<= 8 + datalen |= int(b) + } + + if Debug { + fmt.Printf("Read: datalen = %d numbytes=%d len(buf) = %d\n ", datalen, numBytes, len(buf)) + + for _, b := range buf { + fmt.Printf("%02X ", b) + } + + fmt.Printf("\n") + } + } + + buf = resizeBuffer(buf, idx+datalen) + _, err = io.ReadFull(reader, buf[idx:]) + + if err != nil { + return nil, err + } + + if Debug { + fmt.Printf("Read: len( buf ) = %d idx=%d datalen=%d idx+datalen=%d\n ", len(buf), idx, datalen, idx+datalen) + + for _, b := range buf { + fmt.Printf("%02X ", b) + } + } + + p, _ := decodePacket(buf) + + return p, nil +} + +func DecodeString(data []byte) string { + return string(data) +} + +func parseInt64(bytes []byte) (ret int64, err error) { + if len(bytes) > 8 { + // We'll overflow an int64 in this case. + err = fmt.Errorf("integer too large") + return + } + for bytesRead := 0; bytesRead < len(bytes); bytesRead++ { + ret <<= 8 + ret |= int64(bytes[bytesRead]) + } + + // Shift up and down in order to sign extend the result. + ret <<= 64 - uint8(len(bytes))*8 + ret >>= 64 - uint8(len(bytes))*8 + return +} + +func encodeInteger(i int64) []byte { + n := int64Length(i) + out := make([]byte, n) + + var j int + for ; n > 0; n-- { + out[j] = (byte(i >> uint((n-1)*8))) + j++ + } + + return out +} + +func int64Length(i int64) (numBytes int) { + numBytes = 1 + + for i > 127 { + numBytes++ + i >>= 8 + } + + for i < -128 { + numBytes++ + i >>= 8 + } + + return +} + +func DecodePacket(data []byte) *Packet { + p, _ := decodePacket(data) + + return p +} + +func decodePacket(data []byte) (*Packet, []byte) { + if Debug { + fmt.Printf("decodePacket: enter %d\n", len(data)) + } + + p := new(Packet) + + p.ClassType = Class(data[0]) & ClassBitmask + p.TagType = Type(data[0]) & TypeBitmask + p.Tag = Tag(data[0]) & TagBitmask + + var datalen int + l := data[1] + datapos := 2 + if l&0x80 == 0 { + // The length is encoded in the bottom 7 bits. + datalen = int(l & 0x7f) + } else { + // Bottom 7 bits give the number of length bytes to follow. + numBytes := int(l & 0x7f) + if numBytes == 0 { + return nil, nil + } + datapos += numBytes + datalen = 0 + for i := 0; i < numBytes; i++ { + b := data[2+i] + datalen <<= 8 + datalen |= int(b) + } + } + + p.Data = new(bytes.Buffer) + + p.Children = make([]*Packet, 0, 2) + + p.Value = nil + + value_data := data[datapos : datapos+datalen] + + if p.TagType == TypeConstructed { + for len(value_data) != 0 { + var child *Packet + + child, value_data = decodePacket(value_data) + p.AppendChild(child) + } + } else if p.ClassType == ClassUniversal { + p.Data.Write(data[datapos : datapos+datalen]) + p.ByteValue = value_data + + switch p.Tag { + case TagEOC: + case TagBoolean: + val, _ := parseInt64(value_data) + + p.Value = val != 0 + case TagInteger: + p.Value, _ = parseInt64(value_data) + case TagBitString: + case TagOctetString: + // the actual string encoding is not known here + // (e.g. for LDAP value_data is already an UTF8-encoded + // string). Return the data without further processing + p.Value = DecodeString(value_data) + case TagNULL: + case TagObjectIdentifier: + case TagObjectDescriptor: + case TagExternal: + case TagRealFloat: + case TagEnumerated: + p.Value, _ = parseInt64(value_data) + case TagEmbeddedPDV: + case TagUTF8String: + case TagRelativeOID: + case TagSequence: + case TagSet: + case TagNumericString: + case TagPrintableString: + p.Value = DecodeString(value_data) + case TagT61String: + case TagVideotexString: + case TagIA5String: + case TagUTCTime: + case TagGeneralizedTime: + case TagGraphicString: + case TagVisibleString: + case TagGeneralString: + case TagUniversalString: + case TagCharacterString: + case TagBMPString: + } + } else { + p.Data.Write(data[datapos : datapos+datalen]) + } + + return p, data[datapos+datalen:] +} + +func (p *Packet) Bytes() []byte { + var out bytes.Buffer + + out.Write([]byte{byte(p.ClassType) | byte(p.TagType) | byte(p.Tag)}) + packet_length := encodeInteger(int64(p.Data.Len())) + + if p.Data.Len() > 127 || len(packet_length) > 1 { + out.Write([]byte{byte(len(packet_length) | 128)}) + out.Write(packet_length) + } else { + out.Write(packet_length) + } + + out.Write(p.Data.Bytes()) + + return out.Bytes() +} + +func (p *Packet) AppendChild(child *Packet) { + p.Data.Write(child.Bytes()) + p.Children = append(p.Children, child) +} + +func Encode(ClassType Class, TagType Type, Tag Tag, Value interface{}, Description string) *Packet { + p := new(Packet) + + p.ClassType = ClassType + p.TagType = TagType + p.Tag = Tag + p.Data = new(bytes.Buffer) + + p.Children = make([]*Packet, 0, 2) + + p.Value = Value + p.Description = Description + + if Value != nil { + v := reflect.ValueOf(Value) + + if ClassType == ClassUniversal { + switch Tag { + case TagOctetString: + sv, ok := v.Interface().(string) + + if ok { + p.Data.Write([]byte(sv)) + } + } + } + } + + return p +} + +func NewSequence(Description string) *Packet { + return Encode(ClassUniversal, TypeConstructed, TagSequence, nil, Description) +} + +func NewBoolean(ClassType Class, TagType Type, Tag Tag, Value bool, Description string) *Packet { + intValue := int64(0) + + if Value { + intValue = 1 + } + + p := Encode(ClassType, TagType, Tag, nil, Description) + + p.Value = Value + p.Data.Write(encodeInteger(intValue)) + + return p +} + +func NewInteger(ClassType Class, TagType Type, Tag Tag, Value interface{}, Description string) *Packet { + p := Encode(ClassType, TagType, Tag, nil, Description) + + p.Value = Value + switch v := Value.(type) { + case int: + p.Data.Write(encodeInteger(int64(v))) + case uint: + p.Data.Write(encodeInteger(int64(v))) + case int64: + p.Data.Write(encodeInteger(v)) + case uint64: + // TODO : check range or add encodeUInt... + p.Data.Write(encodeInteger(int64(v))) + case int32: + p.Data.Write(encodeInteger(int64(v))) + case uint32: + p.Data.Write(encodeInteger(int64(v))) + case int16: + p.Data.Write(encodeInteger(int64(v))) + case uint16: + p.Data.Write(encodeInteger(int64(v))) + case int8: + p.Data.Write(encodeInteger(int64(v))) + case uint8: + p.Data.Write(encodeInteger(int64(v))) + default: + // TODO : add support for big.Int ? + panic(fmt.Sprintf("Invalid type %T, expected {u|}int{64|32|16|8}", v)) + } + + return p +} + +func NewString(ClassType Class, TagType Type, Tag Tag, Value, Description string) *Packet { + p := Encode(ClassType, TagType, Tag, nil, Description) + + p.Value = Value + p.Data.Write([]byte(Value)) + + return p +} diff --git a/Godeps/_workspace/src/github.com/vanackere/asn1-ber/ber_test.go b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/ber_test.go new file mode 100644 index 000000000..ccf490688 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/asn1-ber/ber_test.go @@ -0,0 +1,158 @@ +package ber + +import ( + "bytes" + + "io" + "testing" +) + +func TestEncodeDecodeInteger(t *testing.T) { + for _, v := range []int64{0, 10, 128, 1024, -1, -100, -128, -1024} { + enc := encodeInteger(v) + dec, err := parseInt64(enc) + if err != nil { + t.Fatalf("Error decoding %d : %s", v, err) + } + if v != dec { + t.Error("TestEncodeDecodeInteger failed for %d (got %d)", v, dec) + } + + } +} + +func TestBoolean(t *testing.T) { + var value bool = true + + packet := NewBoolean(ClassUniversal, TypePrimitive, TagBoolean, value, "first Packet, True") + + newBoolean, ok := packet.Value.(bool) + if !ok || newBoolean != value { + t.Error("error during creating packet") + } + + encodedPacket := packet.Bytes() + + newPacket := DecodePacket(encodedPacket) + + newBoolean, ok = newPacket.Value.(bool) + if !ok || newBoolean != value { + t.Error("error during decoding packet") + } + +} + +func TestInteger(t *testing.T) { + var value int64 = 10 + + packet := NewInteger(ClassUniversal, TypePrimitive, TagInteger, value, "Integer, 10") + + { + newInteger, ok := packet.Value.(int64) + if !ok || newInteger != value { + t.Error("error creating packet") + } + } + + encodedPacket := packet.Bytes() + + newPacket := DecodePacket(encodedPacket) + + { + newInteger, ok := newPacket.Value.(int64) + if !ok || int64(newInteger) != value { + t.Error("error decoding packet") + } + } +} + +func TestString(t *testing.T) { + var value string = "Hic sunt dracones" + + packet := NewString(ClassUniversal, TypePrimitive, TagOctetString, value, "String") + + newValue, ok := packet.Value.(string) + if !ok || newValue != value { + t.Error("error during creating packet") + } + + encodedPacket := packet.Bytes() + + newPacket := DecodePacket(encodedPacket) + + newValue, ok = newPacket.Value.(string) + if !ok || newValue != value { + t.Error("error during decoding packet") + } + +} + +func TestSequenceAndAppendChild(t *testing.T) { + + p1 := NewString(ClassUniversal, TypePrimitive, TagOctetString, "HIC SVNT LEONES", "String") + p2 := NewString(ClassUniversal, TypePrimitive, TagOctetString, "HIC SVNT DRACONES", "String") + p3 := NewString(ClassUniversal, TypePrimitive, TagOctetString, "Terra Incognita", "String") + + sequence := NewSequence("a sequence") + sequence.AppendChild(p1) + sequence.AppendChild(p2) + sequence.AppendChild(p3) + + if len(sequence.Children) != 3 { + t.Error("wrong length for children array should be three =>", len(sequence.Children)) + } + + encodedSequence := sequence.Bytes() + + decodedSequence := DecodePacket(encodedSequence) + if len(decodedSequence.Children) != 3 { + t.Error("wrong length for children array should be three =>", len(decodedSequence.Children)) + } + +} + +func TestReadPacket(t *testing.T) { + packet := NewString(ClassUniversal, TypePrimitive, TagOctetString, "Ad impossibilia nemo tenetur", "string") + var buffer io.ReadWriter + buffer = new(bytes.Buffer) + + buffer.Write(packet.Bytes()) + + newPacket, err := ReadPacket(buffer) + if err != nil { + t.Error("error during ReadPacket", err) + } + newPacket.ByteValue = nil + if !bytes.Equal(newPacket.ByteValue, packet.ByteValue) { + t.Error("packets should be the same") + } +} + +func TestBinaryInteger(t *testing.T) { + // data src : http://luca.ntop.org/Teaching/Appunti/asn1.html 5.7 + var data = []struct { + v int64 + e []byte + }{ + {v: 0, e: []byte{0x02, 0x01, 0x00}}, + {v: 127, e: []byte{0x02, 0x01, 0x7F}}, + {v: 128, e: []byte{0x02, 0x02, 0x00, 0x80}}, + {v: 256, e: []byte{0x02, 0x02, 0x01, 0x00}}, + {v: -128, e: []byte{0x02, 0x01, 0x80}}, + {v: -129, e: []byte{0x02, 0x02, 0xFF, 0x7F}}, + } + + for _, d := range data { + if b := NewInteger(ClassUniversal, TypePrimitive, TagInteger, int64(d.v), "").Bytes(); !bytes.Equal(d.e, b) { + t.Errorf("Wrong binary generated for %d : got % X, expected % X", d.v, b, d.e) + } + } +} + +func TestBinaryOctetString(t *testing.T) { + // data src : http://luca.ntop.org/Teaching/Appunti/asn1.html 5.10 + + if !bytes.Equal([]byte{0x04, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, NewString(ClassUniversal, TypePrimitive, TagOctetString, "\x01\x23\x45\x67\x89\xab\xcd\xef", "").Bytes()) { + t.Error("wrong binary generated") + } +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/.gitignore b/Godeps/_workspace/src/github.com/vanackere/ldap/.gitignore new file mode 100644 index 000000000..87275bfb6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/.gitignore @@ -0,0 +1,4 @@ +examples/modify +examples/search +examples/searchSSL +examples/searchTLS diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/.travis.yml b/Godeps/_workspace/src/github.com/vanackere/ldap/.travis.yml new file mode 100644 index 000000000..8d7d11f32 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/.travis.yml @@ -0,0 +1,12 @@ +language: go +go: + - 1.2 + - 1.3 + - tip +install: + - go list -f '{{range .Imports}}{{.}} {{end}}' ./... | xargs go get -v + - go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs go get -v + - go get code.google.com/p/go.tools/cmd/cover + - go build -v ./... +script: + - go test -v -cover ./... diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/LICENSE b/Godeps/_workspace/src/github.com/vanackere/ldap/LICENSE new file mode 100644 index 000000000..744875676 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/README.md b/Godeps/_workspace/src/github.com/vanackere/ldap/README.md new file mode 100644 index 000000000..8246cef6b --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/README.md @@ -0,0 +1,37 @@ +[![GoDoc](https://godoc.org/github.com/vanackere/ldap?status.svg)](https://godoc.org/github.com/vanackere/ldap) [![Build Status](https://travis-ci.org/vanackere/ldap.svg)](https://travis-ci.org/vanackere/ldap) + +Basic LDAP v3 functionality for the GO programming language. +------------------------------------------------------------ + +* Required library: + - github.com/vanackere/asn1-ber + +* Working: + - Connecting to LDAP server + - Binding to LDAP server + - Searching for entries + - Compiling string filters to LDAP filters + - Paging Search Results + - Modify Requests / Responses + +* Examples: + - search + - modify + +* Tests Implemented: +- Filter Compile / Decompile + +* TODO: +- Add Requests / Responses +- Delete Requests / Responses +- Modify DN Requests / Responses +- Compare Requests / Responses +- Implement Tests / Benchmarks + + +This feature is disabled at the moment, because in some cases the "Search Request Done" packet will be handled before the last "Search Request Entry": + - Mulitple internal goroutines to handle network traffic + Makes library goroutine safe + Can perform multiple search requests at the same time and return + the results to the proper goroutine. All requests are blocking + requests, so the goroutine does not need special handling diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/bind.go b/Godeps/_workspace/src/github.com/vanackere/ldap/bind.go new file mode 100644 index 000000000..4947ba86f --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/bind.go @@ -0,0 +1,55 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "errors" + + "github.com/vanackere/asn1-ber" +) + +func (l *Conn) Bind(username, password string) error { + messageID := l.nextMessageID() + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(messageID), "MessageID")) + bindRequest := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") + bindRequest.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) + bindRequest.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, username, "User Name")) + bindRequest.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, password, "Password")) + packet.AppendChild(bindRequest) + + if l.Debug { + ber.PrintPacket(packet) + } + + channel, err := l.sendMessage(packet) + if err != nil { + return err + } + if channel == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not send message")) + } + defer l.finishMessage(messageID) + + packet = <-channel + if packet == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not retrieve response")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return NewError(resultCode, errors.New(resultDescription)) + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/conn.go b/Godeps/_workspace/src/github.com/vanackere/ldap/conn.go new file mode 100644 index 000000000..0e28a107f --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/conn.go @@ -0,0 +1,281 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "crypto/tls" + "errors" + "log" + "net" + "sync" + + "github.com/vanackere/asn1-ber" +) + +const ( + MessageQuit = 0 + MessageRequest = 1 + MessageResponse = 2 + MessageFinish = 3 +) + +type messagePacket struct { + Op int + MessageID uint64 + Packet *ber.Packet + Channel chan *ber.Packet +} + +// Conn represents an LDAP Connection +type Conn struct { + conn net.Conn + isTLS bool + Debug debugging + chanConfirm chan bool + chanResults map[uint64]chan *ber.Packet + chanMessage chan *messagePacket + chanMessageID chan uint64 + wgSender sync.WaitGroup + chanDone chan struct{} + once sync.Once +} + +// Dial connects to the given address on the given network using net.Dial +// and then returns a new Conn for the connection. +func Dial(network, addr string) (*Conn, error) { + c, err := net.Dial(network, addr) + if err != nil { + return nil, NewError(ErrorNetwork, err) + } + conn := NewConn(c) + conn.start() + return conn, nil +} + +// DialTLS connects to the given address on the given network using tls.Dial +// and then returns a new Conn for the connection. +func DialTLS(network, addr string, config *tls.Config) (*Conn, error) { + c, err := tls.Dial(network, addr, config) + if err != nil { + return nil, NewError(ErrorNetwork, err) + } + conn := NewConn(c) + conn.isTLS = true + conn.start() + return conn, nil +} + +// NewConn returns a new Conn using conn for network I/O. +func NewConn(conn net.Conn) *Conn { + return &Conn{ + conn: conn, + chanConfirm: make(chan bool), + chanMessageID: make(chan uint64), + chanMessage: make(chan *messagePacket, 10), + chanResults: map[uint64]chan *ber.Packet{}, + chanDone: make(chan struct{}), + } +} + +func (l *Conn) start() { + go l.reader() + go l.processMessages() +} + +// Close closes the connection. +func (l *Conn) Close() { + l.once.Do(func() { + close(l.chanDone) + l.wgSender.Wait() + + l.Debug.Printf("Sending quit message and waiting for confirmation") + l.chanMessage <- &messagePacket{Op: MessageQuit} + <-l.chanConfirm + close(l.chanMessage) + + l.Debug.Printf("Closing network connection") + if err := l.conn.Close(); err != nil { + log.Print(err) + } + }) + <-l.chanDone +} + +// Returns the next available messageID +func (l *Conn) nextMessageID() uint64 { + if l.chanMessageID != nil { + if messageID, ok := <-l.chanMessageID; ok { + return messageID + } + } + return 0 +} + +// StartTLS sends the command to start a TLS session and then creates a new TLS Client +func (l *Conn) StartTLS(config *tls.Config) error { + messageID := l.nextMessageID() + + if l.isTLS { + return NewError(ErrorNetwork, errors.New("ldap: already encrypted")) + } + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(messageID), "MessageID")) + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS") + request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command")) + packet.AppendChild(request) + l.Debug.PrintPacket(packet) + + _, err := l.conn.Write(packet.Bytes()) + if err != nil { + return NewError(ErrorNetwork, err) + } + + packet, err = ber.ReadPacket(l.conn) + if err != nil { + return NewError(ErrorNetwork, err) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Children[0].Value.(int64) == 0 { + conn := tls.Client(l.conn, config) + l.isTLS = true + l.conn = conn + } + + return nil +} + +func (l *Conn) closing() bool { + select { + case <-l.chanDone: + return true + default: + return false + } +} + +func (l *Conn) sendMessage(packet *ber.Packet) (chan *ber.Packet, error) { + if l.closing() { + return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) + } + out := make(chan *ber.Packet) + message := &messagePacket{ + Op: MessageRequest, + MessageID: uint64(packet.Children[0].Value.(int64)), + Packet: packet, + Channel: out, + } + l.sendProcessMessage(message) + return out, nil +} + +func (l *Conn) finishMessage(messageID uint64) { + if l.closing() { + return + } + message := &messagePacket{ + Op: MessageFinish, + MessageID: messageID, + } + l.sendProcessMessage(message) +} + +func (l *Conn) sendProcessMessage(message *messagePacket) bool { + l.wgSender.Add(1) + defer l.wgSender.Done() + + if l.closing() { + return false + } + l.chanMessage <- message + return true +} + +func (l *Conn) processMessages() { + defer func() { + for messageID, channel := range l.chanResults { + l.Debug.Printf("Closing channel for MessageID %d", messageID) + close(channel) + delete(l.chanResults, messageID) + } + close(l.chanMessageID) + l.chanConfirm <- true + close(l.chanConfirm) + }() + + var messageID uint64 = 1 + for { + select { + case l.chanMessageID <- messageID: + messageID++ + case messagePacket, ok := <-l.chanMessage: + if !ok { + l.Debug.Printf("Shutting down - message channel is closed") + return + } + switch messagePacket.Op { + case MessageQuit: + l.Debug.Printf("Shutting down - quit message received") + return + case MessageRequest: + // Add to message list and write to network + l.Debug.Printf("Sending message %d", messagePacket.MessageID) + l.chanResults[messagePacket.MessageID] = messagePacket.Channel + // go routine + buf := messagePacket.Packet.Bytes() + + _, err := l.conn.Write(buf) + if err != nil { + l.Debug.Printf("Error Sending Message: %s", err.Error()) + break + } + case MessageResponse: + l.Debug.Printf("Receiving message %d", messagePacket.MessageID) + if chanResult, ok := l.chanResults[messagePacket.MessageID]; ok { + chanResult <- messagePacket.Packet + } else { + log.Printf("Received unexpected message %d", messagePacket.MessageID) + ber.PrintPacket(messagePacket.Packet) + } + case MessageFinish: + // Remove from message list + l.Debug.Printf("Finished message %d", messagePacket.MessageID) + close(l.chanResults[messagePacket.MessageID]) + delete(l.chanResults, messagePacket.MessageID) + } + } + } +} + +func (l *Conn) reader() { + defer func() { + l.Close() + }() + + for { + packet, err := ber.ReadPacket(l.conn) + if err != nil { + l.Debug.Printf("reader: %s", err.Error()) + return + } + addLDAPDescriptions(packet) + message := &messagePacket{ + Op: MessageResponse, + MessageID: uint64(packet.Children[0].Value.(int64)), + Packet: packet, + } + if !l.sendProcessMessage(message) { + return + } + + } +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/control.go b/Godeps/_workspace/src/github.com/vanackere/ldap/control.go new file mode 100644 index 000000000..8ba714f3b --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/control.go @@ -0,0 +1,157 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "fmt" + + "github.com/vanackere/asn1-ber" +) + +const ( + ControlTypePaging = "1.2.840.113556.1.4.319" +) + +var ControlTypeMap = map[string]string{ + ControlTypePaging: "Paging", +} + +type Control interface { + GetControlType() string + Encode() *ber.Packet + String() string +} + +type ControlString struct { + ControlType string + Criticality bool + ControlValue string +} + +func (c *ControlString) GetControlType() string { + return c.ControlType +} + +func (c *ControlString) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlType, "Control Type ("+ControlTypeMap[c.ControlType]+")")) + if c.Criticality { + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) + } + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlValue, "Control Value")) + return packet +} + +func (c *ControlString) String() string { + return fmt.Sprintf("Control Type: %s (%q) Criticality: %t Control Value: %s", ControlTypeMap[c.ControlType], c.ControlType, c.Criticality, c.ControlValue) +} + +type ControlPaging struct { + PagingSize uint32 + Cookie []byte +} + +func (c *ControlPaging) GetControlType() string { + return ControlTypePaging +} + +func (c *ControlPaging) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypePaging, "Control Type ("+ControlTypeMap[ControlTypePaging]+")")) + + p2 := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Paging)") + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Search Control Value") + seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.PagingSize), "Paging Size")) + cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") + cookie.Value = c.Cookie + cookie.Data.Write(c.Cookie) + seq.AppendChild(cookie) + p2.AppendChild(seq) + + packet.AppendChild(p2) + return packet +} + +func (c *ControlPaging) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t PagingSize: %d Cookie: %q", + ControlTypeMap[ControlTypePaging], + ControlTypePaging, + false, + c.PagingSize, + c.Cookie) +} + +func (c *ControlPaging) SetCookie(cookie []byte) { + c.Cookie = cookie +} + +func FindControl(controls []Control, controlType string) Control { + for _, c := range controls { + if c.GetControlType() == controlType { + return c + } + } + return nil +} + +func DecodeControl(packet *ber.Packet) Control { + ControlType := packet.Children[0].Value.(string) + Criticality := false + + packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" + value := packet.Children[1] + if len(packet.Children) == 3 { + value = packet.Children[2] + packet.Children[1].Description = "Criticality" + Criticality = packet.Children[1].Value.(bool) + } + + value.Description = "Control Value" + switch ControlType { + case ControlTypePaging: + value.Description += " (Paging)" + c := new(ControlPaging) + if value.Value != nil { + valueChildren := ber.DecodePacket(value.Data.Bytes()) + value.Data.Truncate(0) + value.Value = nil + value.AppendChild(valueChildren) + } + value = value.Children[0] + value.Description = "Search Control Value" + value.Children[0].Description = "Paging Size" + value.Children[1].Description = "Cookie" + c.PagingSize = uint32(value.Children[0].Value.(int64)) + c.Cookie = value.Children[1].Data.Bytes() + value.Children[1].Value = c.Cookie + return c + } + c := new(ControlString) + c.ControlType = ControlType + c.Criticality = Criticality + c.ControlValue = value.Value.(string) + return c +} + +func NewControlString(controlType string, criticality bool, controlValue string) *ControlString { + return &ControlString{ + ControlType: controlType, + Criticality: criticality, + ControlValue: controlValue, + } +} + +func NewControlPaging(pagingSize uint32) *ControlPaging { + return &ControlPaging{PagingSize: pagingSize} +} + +func encodeControls(controls []Control) *ber.Packet { + packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") + for _, control := range controls { + packet.AppendChild(control.Encode()) + } + return packet +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/debug.go b/Godeps/_workspace/src/github.com/vanackere/ldap/debug.go new file mode 100644 index 000000000..e6edfd4da --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/debug.go @@ -0,0 +1,24 @@ +package ldap + +import ( + "log" + + "github.com/vanackere/asn1-ber" +) + +// debbuging type +// - has a Printf method to write the debug output +type debugging bool + +// write debug output +func (debug debugging) Printf(format string, args ...interface{}) { + if debug { + log.Printf(format, args...) + } +} + +func (debug debugging) PrintPacket(packet *ber.Packet) { + if debug { + ber.PrintPacket(packet) + } +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/examples/enterprise.ldif b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/enterprise.ldif new file mode 100644 index 000000000..f0ec28f16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/enterprise.ldif @@ -0,0 +1,63 @@ +dn: dc=enterprise,dc=org +objectClass: dcObject +objectClass: organization +o: acme + +dn: cn=admin,dc=enterprise,dc=org +objectClass: person +cn: admin +sn: admin +description: "LDAP Admin" + +dn: ou=crew,dc=enterprise,dc=org +ou: crew +objectClass: organizationalUnit + + +dn: cn=kirkj,ou=crew,dc=enterprise,dc=org +cn: kirkj +sn: Kirk +gn: James Tiberius +mail: james.kirk@enterprise.org +objectClass: inetOrgPerson + +dn: cn=spock,ou=crew,dc=enterprise,dc=org +cn: spock +sn: Spock +mail: spock@enterprise.org +objectClass: inetOrgPerson + +dn: cn=mccoyl,ou=crew,dc=enterprise,dc=org +cn: mccoyl +sn: McCoy +gn: Leonard +mail: leonard.mccoy@enterprise.org +objectClass: inetOrgPerson + +dn: cn=scottm,ou=crew,dc=enterprise,dc=org +cn: scottm +sn: Scott +gn: Montgomery +mail: Montgomery.scott@enterprise.org +objectClass: inetOrgPerson + +dn: cn=uhuran,ou=crew,dc=enterprise,dc=org +cn: uhuran +sn: Uhura +gn: Nyota +mail: nyota.uhura@enterprise.org +objectClass: inetOrgPerson + +dn: cn=suluh,ou=crew,dc=enterprise,dc=org +cn: suluh +sn: Sulu +gn: Hikaru +mail: hikaru.sulu@enterprise.org +objectClass: inetOrgPerson + +dn: cn=chekovp,ou=crew,dc=enterprise,dc=org +cn: chekovp +sn: Chekov +gn: pavel +mail: pavel.chekov@enterprise.org +objectClass: inetOrgPerson diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/examples/modify.go b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/modify.go new file mode 100644 index 000000000..326598c4e --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/modify.go @@ -0,0 +1,91 @@ +// +build ignore + +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "fmt" + "log" + + "github.com/vanackere/ldap" +) + +var ( + LdapServer string = "localhost" + LdapPort uint16 = 389 + BaseDN string = "dc=enterprise,dc=org" + BindDN string = "cn=admin,dc=enterprise,dc=org" + BindPW string = "enterprise" + Filter string = "(cn=kirkj)" +) + +func search(l *ldap.Conn, filter string, attributes []string) (*ldap.Entry, *ldap.Error) { + search := ldap.NewSearchRequest( + BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, + attributes, + nil) + + sr, err := l.Search(search) + if err != nil { + log.Fatalf("ERROR: %s\n", err) + return nil, err + } + + log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) + if len(sr.Entries) == 0 { + return nil, ldap.NewError(ldap.ErrorDebugging, errors.New(fmt.Sprintf("no entries found for: %s", filter))) + } + return sr.Entries[0], nil +} + +func main() { + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort)) + if err != nil { + log.Fatalf("ERROR: %s\n", err.Error()) + } + defer l.Close() + // l.Debug = true + + l.Bind(BindDN, BindPW) + + log.Printf("The Search for Kirk ... %s\n", Filter) + entry, err := search(l, Filter, []string{}) + if err != nil { + log.Fatal("could not get entry") + } + entry.PrettyPrint(0) + + log.Printf("modify the mail address and add a description ... \n") + modify := ldap.NewModifyRequest(entry.DN) + modify.Add("description", []string{"Captain of the USS Enterprise"}) + modify.Replace("mail", []string{"captain@enterprise.org"}) + if err := l.Modify(modify); err != nil { + log.Fatalf("ERROR: %s\n", err.Error()) + } + + entry, err = search(l, Filter, []string{}) + if err != nil { + log.Fatal("could not get entry") + } + entry.PrettyPrint(0) + + log.Printf("reset the entry ... \n") + modify = ldap.NewModifyRequest(entry.DN) + modify.Delete("description", []string{}) + modify.Replace("mail", []string{"james.kirk@enterprise.org"}) + if err := l.Modify(modify); err != nil { + log.Fatalf("ERROR: %s\n", err.Error()) + } + + entry, err = search(l, Filter, []string{}) + if err != nil { + log.Fatal("could not get entry") + } + entry.PrettyPrint(0) +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/examples/search.go b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/search.go new file mode 100644 index 000000000..93e941c55 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/search.go @@ -0,0 +1,54 @@ +// +build ignore + +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "log" + + "github.com/vanackere/ldap" +) + +var ( + ldapServer string = "adserver" + ldapPort uint16 = 3268 + baseDN string = "dc=*,dc=*" + filter string = "(&(objectClass=user)(sAMAccountName=*)(memberOf=CN=*,OU=*,DC=*,DC=*))" + Attributes []string = []string{"memberof"} + user string = "*" + passwd string = "*" +) + +func main() { + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + if err != nil { + log.Fatalf("ERROR: %s\n", err.Error()) + } + defer l.Close() + // l.Debug = true + + err = l.Bind(user, passwd) + if err != nil { + log.Printf("ERROR: Cannot bind: %s\n", err.Error()) + return + } + search := ldap.NewSearchRequest( + baseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, + Attributes, + nil) + + sr, err := l.Search(search) + if err != nil { + log.Fatalf("ERROR: %s\n", err.Error()) + return + } + + log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) + sr.PrettyPrint(0) +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/examples/searchSSL.go b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/searchSSL.go new file mode 100644 index 000000000..db2f7b88b --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/searchSSL.go @@ -0,0 +1,47 @@ +// +build ignore + +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "log" + + "github.com/vanackere/ldap" +) + +var ( + LdapServer string = "localhost" + LdapPort uint16 = 636 + BaseDN string = "dc=enterprise,dc=org" + Filter string = "(cn=kirkj)" + Attributes []string = []string{"mail"} +) + +func main() { + l, err := ldap.DialSSL("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort), nil) + if err != nil { + log.Fatalf("ERROR: %s\n", err.String()) + } + defer l.Close() + // l.Debug = true + + search := ldap.NewSearchRequest( + BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + Filter, + Attributes, + nil) + + sr, err := l.Search(search) + if err != nil { + log.Fatalf("ERROR: %s\n", err.String()) + return + } + + log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) + sr.PrettyPrint(0) +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/examples/searchTLS.go b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/searchTLS.go new file mode 100644 index 000000000..b4dce8a95 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/searchTLS.go @@ -0,0 +1,47 @@ +// +build ignore + +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "log" + + "github.com/vanackere/ldap" +) + +var ( + LdapServer string = "localhost" + LdapPort uint16 = 389 + BaseDN string = "dc=enterprise,dc=org" + Filter string = "(cn=kirkj)" + Attributes []string = []string{"mail"} +) + +func main() { + l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort), nil) + if err != nil { + log.Fatalf("ERROR: %s\n", err.Error()) + } + defer l.Close() + // l.Debug = true + + search := ldap.NewSearchRequest( + BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + Filter, + Attributes, + nil) + + sr, err := l.Search(search) + if err != nil { + log.Fatalf("ERROR: %s\n", err.Error()) + return + } + + log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) + sr.PrettyPrint(0) +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/examples/slapd.conf b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/slapd.conf new file mode 100644 index 000000000..5a66be015 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/examples/slapd.conf @@ -0,0 +1,67 @@ +# +# See slapd.conf(5) for details on configuration options. +# This file should NOT be world readable. +# +include /private/etc/openldap/schema/core.schema +include /private/etc/openldap/schema/cosine.schema +include /private/etc/openldap/schema/inetorgperson.schema + +# Define global ACLs to disable default read access. + +# Do not enable referrals until AFTER you have a working directory +# service AND an understanding of referrals. +#referral ldap://root.openldap.org + +pidfile /private/var/db/openldap/run/slapd.pid +argsfile /private/var/db/openldap/run/slapd.args + +# Load dynamic backend modules: +# modulepath /usr/libexec/openldap +# moduleload back_bdb.la +# moduleload back_hdb.la +# moduleload back_ldap.la + +# Sample security restrictions +# Require integrity protection (prevent hijacking) +# Require 112-bit (3DES or better) encryption for updates +# Require 63-bit encryption for simple bind +# security ssf=1 update_ssf=112 simple_bind=64 + +# Sample access control policy: +# Root DSE: allow anyone to read it +# Subschema (sub)entry DSE: allow anyone to read it +# Other DSEs: +# Allow self write access +# Allow authenticated users read access +# Allow anonymous users to authenticate +# Directives needed to implement policy: +# access to dn.base="" by * read +# access to dn.base="cn=Subschema" by * read +# access to * +# by self write +# by users read +# by anonymous auth +# +# if no access controls are present, the default policy +# allows anyone and everyone to read anything but restricts +# updates to rootdn. (e.g., "access to * by * read") +# +# rootdn can always read and write EVERYTHING! + +####################################################################### +# BDB database definitions +####################################################################### + +database bdb +suffix "dc=enterprise,dc=org" +rootdn "cn=admin,dc=enterprise,dc=org" +# Cleartext passwords, especially for the rootdn, should +# be avoid. See slappasswd(8) and slapd.conf(5) for details. +# Use of strong authentication encouraged. +rootpw {SSHA}laO00HsgszhK1O0Z5qR0/i/US69Osfeu +# The database directory MUST exist prior to running slapd AND +# should only be accessible by the slapd and slap tools. +# Mode 700 recommended. +directory /private/var/db/openldap/openldap-data +# Indices to maintain +index objectClass eq diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/filter.go b/Godeps/_workspace/src/github.com/vanackere/ldap/filter.go new file mode 100644 index 000000000..8f0931652 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/filter.go @@ -0,0 +1,247 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "errors" + "fmt" + + "github.com/vanackere/asn1-ber" +) + +const ( + FilterAnd ber.Tag = 0 + FilterOr ber.Tag = 1 + FilterNot ber.Tag = 2 + FilterEqualityMatch ber.Tag = 3 + FilterSubstrings ber.Tag = 4 + FilterGreaterOrEqual ber.Tag = 5 + FilterLessOrEqual ber.Tag = 6 + FilterPresent ber.Tag = 7 + FilterApproxMatch ber.Tag = 8 + FilterExtensibleMatch ber.Tag = 9 +) + +var filterMap = map[ber.Tag]string{ + FilterAnd: "And", + FilterOr: "Or", + FilterNot: "Not", + FilterEqualityMatch: "Equality Match", + FilterSubstrings: "Substrings", + FilterGreaterOrEqual: "Greater Or Equal", + FilterLessOrEqual: "Less Or Equal", + FilterPresent: "Present", + FilterApproxMatch: "Approx Match", + FilterExtensibleMatch: "Extensible Match", +} + +const ( + FilterSubstringsInitial = 0 + FilterSubstringsAny = 1 + FilterSubstringsFinal = 2 +) + +func CompileFilter(filter string) (*ber.Packet, error) { + if len(filter) == 0 || filter[0] != '(' { + return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('")) + } + packet, pos, err := compileFilter(filter, 1) + if err != nil { + return nil, err + } + if pos != len(filter) { + return nil, NewError(ErrorFilterCompile, errors.New("ldap: finished compiling filter with extra at end: "+fmt.Sprint(filter[pos:]))) + } + return packet, nil +} + +func DecompileFilter(packet *ber.Packet) (ret string, err error) { + defer func() { + if r := recover(); r != nil { + err = NewError(ErrorFilterDecompile, errors.New("ldap: error decompiling filter")) + } + }() + ret = "(" + err = nil + childStr := "" + + switch packet.Tag { + case FilterAnd: + ret += "&" + for _, child := range packet.Children { + childStr, err = DecompileFilter(child) + if err != nil { + return + } + ret += childStr + } + case FilterOr: + ret += "|" + for _, child := range packet.Children { + childStr, err = DecompileFilter(child) + if err != nil { + return + } + ret += childStr + } + case FilterNot: + ret += "!" + childStr, err = DecompileFilter(packet.Children[0]) + if err != nil { + return + } + ret += childStr + + case FilterSubstrings: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += "=" + switch packet.Children[1].Children[0].Tag { + case FilterSubstringsInitial: + ret += ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) + "*" + case FilterSubstringsAny: + ret += "*" + ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) + "*" + case FilterSubstringsFinal: + ret += "*" + ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) + } + case FilterEqualityMatch: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += "=" + ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + case FilterGreaterOrEqual: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += ">=" + ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + case FilterLessOrEqual: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += "<=" + ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + case FilterPresent: + ret += ber.DecodeString(packet.Data.Bytes()) + ret += "=*" + case FilterApproxMatch: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += "~=" + ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + } + + ret += ")" + return +} + +func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { + for pos < len(filter) && filter[pos] == '(' { + child, newPos, err := compileFilter(filter, pos+1) + if err != nil { + return pos, err + } + pos = newPos + parent.AppendChild(child) + } + if pos == len(filter) { + return pos, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) + } + + return pos + 1, nil +} + +func compileFilter(filter string, pos int) (*ber.Packet, int, error) { + var packet *ber.Packet + var err error + + defer func() { + if r := recover(); r != nil { + err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter")) + } + }() + + newPos := pos + switch filter[pos] { + case '(': + packet, newPos, err = compileFilter(filter, pos+1) + newPos++ + return packet, newPos, err + case '&': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, filterMap[FilterAnd]) + newPos, err = compileFilterSet(filter, pos+1, packet) + return packet, newPos, err + case '|': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, filterMap[FilterOr]) + newPos, err = compileFilterSet(filter, pos+1, packet) + return packet, newPos, err + case '!': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, filterMap[FilterNot]) + var child *ber.Packet + child, newPos, err = compileFilter(filter, pos+1) + packet.AppendChild(child) + return packet, newPos, err + default: + attribute := "" + condition := "" + for newPos < len(filter) && filter[newPos] != ')' { + switch { + case packet != nil: + condition += fmt.Sprintf("%c", filter[newPos]) + case filter[newPos] == '=': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, filterMap[FilterEqualityMatch]) + case filter[newPos] == '>' && filter[newPos+1] == '=': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, filterMap[FilterGreaterOrEqual]) + newPos++ + case filter[newPos] == '<' && filter[newPos+1] == '=': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, filterMap[FilterLessOrEqual]) + newPos++ + case filter[newPos] == '~' && filter[newPos+1] == '=': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, filterMap[FilterLessOrEqual]) + newPos++ + case packet == nil: + attribute += fmt.Sprintf("%c", filter[newPos]) + } + newPos++ + } + if newPos == len(filter) { + err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) + return packet, newPos, err + } + if packet == nil { + err = NewError(ErrorFilterCompile, errors.New("ldap: error parsing filter")) + return packet, newPos, err + } + // Handle FilterEqualityMatch as a separate case (is primitive, not constructed like the other filters) + if packet.Tag == FilterEqualityMatch && condition == "*" { + packet.TagType = ber.TypePrimitive + packet.Tag = FilterPresent + packet.Description = filterMap[packet.Tag] + packet.Data.WriteString(attribute) + return packet, newPos + 1, nil + } + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) + switch { + case packet.Tag == FilterEqualityMatch && condition[0] == '*' && condition[len(condition)-1] == '*': + // Any + packet.Tag = FilterSubstrings + packet.Description = filterMap[packet.Tag] + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") + seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsAny, condition[1:len(condition)-1], "Any Substring")) + packet.AppendChild(seq) + case packet.Tag == FilterEqualityMatch && condition[0] == '*': + // Final + packet.Tag = FilterSubstrings + packet.Description = filterMap[packet.Tag] + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") + seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsFinal, condition[1:], "Final Substring")) + packet.AppendChild(seq) + case packet.Tag == FilterEqualityMatch && condition[len(condition)-1] == '*': + // Initial + packet.Tag = FilterSubstrings + packet.Description = filterMap[packet.Tag] + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") + seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsInitial, condition[:len(condition)-1], "Initial Substring")) + packet.AppendChild(seq) + default: + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, condition, "Condition")) + } + newPos++ + return packet, newPos, err + } +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/filter_test.go b/Godeps/_workspace/src/github.com/vanackere/ldap/filter_test.go new file mode 100644 index 000000000..b59232e95 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/filter_test.go @@ -0,0 +1,113 @@ +package ldap + +import ( + "reflect" + "testing" + + "github.com/vanackere/asn1-ber" +) + +type compileTest struct { + filterStr string + filterType ber.Tag +} + +var testFilters = []compileTest{ + compileTest{filterStr: "(&(sn=Miller)(givenName=Bob))", filterType: FilterAnd}, + compileTest{filterStr: "(|(sn=Miller)(givenName=Bob))", filterType: FilterOr}, + compileTest{filterStr: "(!(sn=Miller))", filterType: FilterNot}, + compileTest{filterStr: "(sn=Miller)", filterType: FilterEqualityMatch}, + compileTest{filterStr: "(sn=Mill*)", filterType: FilterSubstrings}, + compileTest{filterStr: "(sn=*Mill)", filterType: FilterSubstrings}, + compileTest{filterStr: "(sn=*Mill*)", filterType: FilterSubstrings}, + compileTest{filterStr: "(sn>=Miller)", filterType: FilterGreaterOrEqual}, + compileTest{filterStr: "(sn<=Miller)", filterType: FilterLessOrEqual}, + compileTest{filterStr: "(sn=*)", filterType: FilterPresent}, + compileTest{filterStr: "(sn~=Miller)", filterType: FilterApproxMatch}, + // compileTest{ filterStr: "()", filterType: FilterExtensibleMatch }, +} + +func TestFilter(t *testing.T) { + // Test Compiler and Decompiler + for _, i := range testFilters { + filter, err := CompileFilter(i.filterStr) + if err != nil { + t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) + } else if filter.Tag != i.filterType { + t.Errorf("%q Expected %q got %q", i.filterStr, filterMap[i.filterType], filterMap[filter.Tag]) + } else { + o, err := DecompileFilter(filter) + if err != nil { + t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) + } else if i.filterStr != o { + t.Errorf("%q expected, got %q", i.filterStr, o) + } + } + } +} + +type binTestFilter struct { + bin []byte + str string +} + +var binTestFilters = []binTestFilter{ + {bin: []byte{0x87, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72}, str: "(member=*)"}, +} + +func TestFiltersDecode(t *testing.T) { + for i, test := range binTestFilters { + p := ber.DecodePacket(test.bin) + if filter, err := DecompileFilter(p); err != nil { + t.Errorf("binTestFilters[%d], DecompileFilter returned : %s", i, err) + } else if filter != test.str { + t.Errorf("binTestFilters[%d], %q expected, got %q", i, test.str, filter) + } + } +} + +func TestFiltersEncode(t *testing.T) { + for i, test := range binTestFilters { + p, err := CompileFilter(test.str) + if err != nil { + t.Errorf("binTestFilters[%d], CompileFilter returned : %s", i, err) + continue + } + b := p.Bytes() + if !reflect.DeepEqual(b, test.bin) { + t.Errorf("binTestFilters[%d], %q expected for CompileFilter(%q), got %q", i, test.bin, test.str, b) + } + } +} + +func BenchmarkFilterCompile(b *testing.B) { + b.StopTimer() + filters := make([]string, len(testFilters)) + + // Test Compiler and Decompiler + for idx, i := range testFilters { + filters[idx] = i.filterStr + } + + maxIdx := len(filters) + b.StartTimer() + for i := 0; i < b.N; i++ { + CompileFilter(filters[i%maxIdx]) + } +} + +func BenchmarkFilterDecompile(b *testing.B) { + b.StopTimer() + filters := make([]*ber.Packet, len(testFilters)) + + // Test Compiler and Decompiler + for idx, i := range testFilters { + filters[idx], _ = CompileFilter(i.filterStr) + } + + maxIdx := len(filters) + b.StartTimer() + for i := 0; i < b.N; i++ { + DecompileFilter(filters[i%maxIdx]) + } +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/ldap.go b/Godeps/_workspace/src/github.com/vanackere/ldap/ldap.go new file mode 100644 index 000000000..6259bb01a --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/ldap.go @@ -0,0 +1,303 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/vanackere/asn1-ber" +) + +// LDAP Application Codes +const ( + ApplicationBindRequest ber.Tag = 0 + ApplicationBindResponse ber.Tag = 1 + ApplicationUnbindRequest ber.Tag = 2 + ApplicationSearchRequest ber.Tag = 3 + ApplicationSearchResultEntry ber.Tag = 4 + ApplicationSearchResultDone ber.Tag = 5 + ApplicationModifyRequest ber.Tag = 6 + ApplicationModifyResponse ber.Tag = 7 + ApplicationAddRequest ber.Tag = 8 + ApplicationAddResponse ber.Tag = 9 + ApplicationDelRequest ber.Tag = 10 + ApplicationDelResponse ber.Tag = 11 + ApplicationModifyDNRequest ber.Tag = 12 + ApplicationModifyDNResponse ber.Tag = 13 + ApplicationCompareRequest ber.Tag = 14 + ApplicationCompareResponse ber.Tag = 15 + ApplicationAbandonRequest ber.Tag = 16 + ApplicationSearchResultReference ber.Tag = 19 + ApplicationExtendedRequest ber.Tag = 23 + ApplicationExtendedResponse ber.Tag = 24 +) + +var ApplicationMap = map[ber.Tag]string{ + ApplicationBindRequest: "Bind Request", + ApplicationBindResponse: "Bind Response", + ApplicationUnbindRequest: "Unbind Request", + ApplicationSearchRequest: "Search Request", + ApplicationSearchResultEntry: "Search Result Entry", + ApplicationSearchResultDone: "Search Result Done", + ApplicationModifyRequest: "Modify Request", + ApplicationModifyResponse: "Modify Response", + ApplicationAddRequest: "Add Request", + ApplicationAddResponse: "Add Response", + ApplicationDelRequest: "Del Request", + ApplicationDelResponse: "Del Response", + ApplicationModifyDNRequest: "Modify DN Request", + ApplicationModifyDNResponse: "Modify DN Response", + ApplicationCompareRequest: "Compare Request", + ApplicationCompareResponse: "Compare Response", + ApplicationAbandonRequest: "Abandon Request", + ApplicationSearchResultReference: "Search Result Reference", + ApplicationExtendedRequest: "Extended Request", + ApplicationExtendedResponse: "Extended Response", +} + +// LDAP Result Codes +const ( + LDAPResultSuccess = 0 + LDAPResultOperationsError = 1 + LDAPResultProtocolError = 2 + LDAPResultTimeLimitExceeded = 3 + LDAPResultSizeLimitExceeded = 4 + LDAPResultCompareFalse = 5 + LDAPResultCompareTrue = 6 + LDAPResultAuthMethodNotSupported = 7 + LDAPResultStrongAuthRequired = 8 + LDAPResultReferral = 10 + LDAPResultAdminLimitExceeded = 11 + LDAPResultUnavailableCriticalExtension = 12 + LDAPResultConfidentialityRequired = 13 + LDAPResultSaslBindInProgress = 14 + LDAPResultNoSuchAttribute = 16 + LDAPResultUndefinedAttributeType = 17 + LDAPResultInappropriateMatching = 18 + LDAPResultConstraintViolation = 19 + LDAPResultAttributeOrValueExists = 20 + LDAPResultInvalidAttributeSyntax = 21 + LDAPResultNoSuchObject = 32 + LDAPResultAliasProblem = 33 + LDAPResultInvalidDNSyntax = 34 + LDAPResultAliasDereferencingProblem = 36 + LDAPResultInappropriateAuthentication = 48 + LDAPResultInvalidCredentials = 49 + LDAPResultInsufficientAccessRights = 50 + LDAPResultBusy = 51 + LDAPResultUnavailable = 52 + LDAPResultUnwillingToPerform = 53 + LDAPResultLoopDetect = 54 + LDAPResultNamingViolation = 64 + LDAPResultObjectClassViolation = 65 + LDAPResultNotAllowedOnNonLeaf = 66 + LDAPResultNotAllowedOnRDN = 67 + LDAPResultEntryAlreadyExists = 68 + LDAPResultObjectClassModsProhibited = 69 + LDAPResultAffectsMultipleDSAs = 71 + LDAPResultOther = 80 + + ErrorNetwork = 200 + ErrorFilterCompile = 201 + ErrorFilterDecompile = 202 + ErrorDebugging = 203 +) + +var LDAPResultCodeMap = map[uint8]string{ + LDAPResultSuccess: "Success", + LDAPResultOperationsError: "Operations Error", + LDAPResultProtocolError: "Protocol Error", + LDAPResultTimeLimitExceeded: "Time Limit Exceeded", + LDAPResultSizeLimitExceeded: "Size Limit Exceeded", + LDAPResultCompareFalse: "Compare False", + LDAPResultCompareTrue: "Compare True", + LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", + LDAPResultStrongAuthRequired: "Strong Auth Required", + LDAPResultReferral: "Referral", + LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", + LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", + LDAPResultConfidentialityRequired: "Confidentiality Required", + LDAPResultSaslBindInProgress: "Sasl Bind In Progress", + LDAPResultNoSuchAttribute: "No Such Attribute", + LDAPResultUndefinedAttributeType: "Undefined Attribute Type", + LDAPResultInappropriateMatching: "Inappropriate Matching", + LDAPResultConstraintViolation: "Constraint Violation", + LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", + LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", + LDAPResultNoSuchObject: "No Such Object", + LDAPResultAliasProblem: "Alias Problem", + LDAPResultInvalidDNSyntax: "Invalid DN Syntax", + LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", + LDAPResultInappropriateAuthentication: "Inappropriate Authentication", + LDAPResultInvalidCredentials: "Invalid Credentials", + LDAPResultInsufficientAccessRights: "Insufficient Access Rights", + LDAPResultBusy: "Busy", + LDAPResultUnavailable: "Unavailable", + LDAPResultUnwillingToPerform: "Unwilling To Perform", + LDAPResultLoopDetect: "Loop Detect", + LDAPResultNamingViolation: "Naming Violation", + LDAPResultObjectClassViolation: "Object Class Violation", + LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", + LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", + LDAPResultEntryAlreadyExists: "Entry Already Exists", + LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", + LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", + LDAPResultOther: "Other", +} + +// Adds descriptions to an LDAP Response packet for debugging +func addLDAPDescriptions(packet *ber.Packet) (err error) { + defer func() { + if r := recover(); r != nil { + err = NewError(ErrorDebugging, errors.New("ldap: cannot process packet to add descriptions")) + } + }() + packet.Description = "LDAP Response" + packet.Children[0].Description = "Message ID" + + application := packet.Children[1].Tag + packet.Children[1].Description = ApplicationMap[application] + + switch application { + case ApplicationBindRequest: + addRequestDescriptions(packet) + case ApplicationBindResponse: + addDefaultLDAPResponseDescriptions(packet) + case ApplicationUnbindRequest: + addRequestDescriptions(packet) + case ApplicationSearchRequest: + addRequestDescriptions(packet) + case ApplicationSearchResultEntry: + packet.Children[1].Children[0].Description = "Object Name" + packet.Children[1].Children[1].Description = "Attributes" + for _, child := range packet.Children[1].Children[1].Children { + child.Description = "Attribute" + child.Children[0].Description = "Attribute Name" + child.Children[1].Description = "Attribute Values" + for _, grandchild := range child.Children[1].Children { + grandchild.Description = "Attribute Value" + } + } + if len(packet.Children) == 3 { + addControlDescriptions(packet.Children[2]) + } + case ApplicationSearchResultDone: + addDefaultLDAPResponseDescriptions(packet) + case ApplicationModifyRequest: + addRequestDescriptions(packet) + case ApplicationModifyResponse: + case ApplicationAddRequest: + addRequestDescriptions(packet) + case ApplicationAddResponse: + case ApplicationDelRequest: + addRequestDescriptions(packet) + case ApplicationDelResponse: + case ApplicationModifyDNRequest: + addRequestDescriptions(packet) + case ApplicationModifyDNResponse: + case ApplicationCompareRequest: + addRequestDescriptions(packet) + case ApplicationCompareResponse: + case ApplicationAbandonRequest: + addRequestDescriptions(packet) + case ApplicationSearchResultReference: + case ApplicationExtendedRequest: + addRequestDescriptions(packet) + case ApplicationExtendedResponse: + } + + return nil +} + +func addControlDescriptions(packet *ber.Packet) { + packet.Description = "Controls" + for _, child := range packet.Children { + child.Description = "Control" + child.Children[0].Description = "Control Type (" + ControlTypeMap[child.Children[0].Value.(string)] + ")" + value := child.Children[1] + if len(child.Children) == 3 { + child.Children[1].Description = "Criticality" + value = child.Children[2] + } + value.Description = "Control Value" + + switch child.Children[0].Value.(string) { + case ControlTypePaging: + value.Description += " (Paging)" + if value.Value != nil { + valueChildren := ber.DecodePacket(value.Data.Bytes()) + value.Data.Truncate(0) + value.Value = nil + valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes() + value.AppendChild(valueChildren) + } + value.Children[0].Description = "Real Search Control Value" + value.Children[0].Children[0].Description = "Paging Size" + value.Children[0].Children[1].Description = "Cookie" + } + } +} + +func addRequestDescriptions(packet *ber.Packet) { + packet.Description = "LDAP Request" + packet.Children[0].Description = "Message ID" + packet.Children[1].Description = ApplicationMap[packet.Children[1].Tag] + if len(packet.Children) == 3 { + addControlDescriptions(packet.Children[2]) + } +} + +func addDefaultLDAPResponseDescriptions(packet *ber.Packet) { + resultCode := packet.Children[1].Children[0].Value.(int64) + packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[uint8(resultCode)] + ")" + packet.Children[1].Children[1].Description = "Matched DN" + packet.Children[1].Children[2].Description = "Error Message" + if len(packet.Children[1].Children) > 3 { + packet.Children[1].Children[3].Description = "Referral" + } + if len(packet.Children) == 3 { + addControlDescriptions(packet.Children[2]) + } +} + +func DebugBinaryFile(fileName string) error { + file, err := ioutil.ReadFile(fileName) + if err != nil { + return NewError(ErrorDebugging, err) + } + ber.PrintBytes(os.Stdout, file, "") + packet := ber.DecodePacket(file) + addLDAPDescriptions(packet) + ber.PrintPacket(packet) + + return nil +} + +type Error struct { + Err error + ResultCode uint8 +} + +func (e *Error) Error() string { + return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) +} + +func NewError(resultCode uint8, err error) error { + return &Error{ResultCode: resultCode, Err: err} +} + +func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) { + if len(packet.Children) >= 2 { + response := packet.Children[1] + if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) == 3 { + return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string) + } + } + + return ErrorNetwork, "Invalid packet format" +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/ldap_test.go b/Godeps/_workspace/src/github.com/vanackere/ldap/ldap_test.go new file mode 100644 index 000000000..31cfbf02f --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/ldap_test.go @@ -0,0 +1,123 @@ +package ldap + +import ( + "fmt" + "testing" +) + +var ldapServer = "ldap.itd.umich.edu" +var ldapPort = uint16(389) +var baseDN = "dc=umich,dc=edu" +var filter = []string{ + "(cn=cis-fac)", + "(&(objectclass=rfc822mailgroup)(cn=*Computer*))", + "(&(objectclass=rfc822mailgroup)(cn=*Mathematics*))"} +var attributes = []string{ + "cn", + "description"} + +func TestConnect(t *testing.T) { + fmt.Printf("TestConnect: starting...\n") + l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + if err != nil { + t.Errorf(err.Error()) + return + } + defer l.Close() + fmt.Printf("TestConnect: finished...\n") +} + +func TestSearch(t *testing.T) { + fmt.Printf("TestSearch: starting...\n") + l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + if err != nil { + t.Errorf(err.Error()) + return + } + defer l.Close() + + searchRequest := NewSearchRequest( + baseDN, + ScopeWholeSubtree, DerefAlways, 0, 0, false, + filter[0], + attributes, + nil) + + sr, err := l.Search(searchRequest) + if err != nil { + t.Errorf(err.Error()) + return + } + + fmt.Printf("TestSearch: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) +} + +func TestSearchWithPaging(t *testing.T) { + fmt.Printf("TestSearchWithPaging: starting...\n") + l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + if err != nil { + t.Errorf(err.Error()) + return + } + defer l.Close() + + err = l.Bind("", "") + if err != nil { + t.Errorf(err.Error()) + return + } + + searchRequest := NewSearchRequest( + baseDN, + ScopeWholeSubtree, DerefAlways, 0, 0, false, + filter[1], + attributes, + nil) + sr, err := l.SearchWithPaging(searchRequest, 5) + if err != nil { + t.Errorf(err.Error()) + return + } + + fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) +} + +func testMultiGoroutineSearch(t *testing.T, l *Conn, results chan *SearchResult, i int) { + searchRequest := NewSearchRequest( + baseDN, + ScopeWholeSubtree, DerefAlways, 0, 0, false, + filter[i], + attributes, + nil) + sr, err := l.Search(searchRequest) + if err != nil { + t.Errorf(err.Error()) + results <- nil + return + } + results <- sr +} + +func TestMultiGoroutineSearch(t *testing.T) { + fmt.Printf("TestMultiGoroutineSearch: starting...\n") + l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + if err != nil { + t.Errorf(err.Error()) + return + } + defer l.Close() + + results := make([]chan *SearchResult, len(filter)) + for i := range filter { + results[i] = make(chan *SearchResult) + go testMultiGoroutineSearch(t, l, results[i], i) + } + for i := range filter { + sr := <-results[i] + if sr == nil { + t.Errorf("Did not receive results from goroutine for %q", filter[i]) + } else { + fmt.Printf("TestMultiGoroutineSearch(%d): %s -> num of entries = %d\n", i, filter[i], len(sr.Entries)) + } + } +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/modify.go b/Godeps/_workspace/src/github.com/vanackere/ldap/modify.go new file mode 100644 index 000000000..2070af1ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/modify.go @@ -0,0 +1,156 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// File contains Modify functionality +// +// https://tools.ietf.org/html/rfc4511 +// +// ModifyRequest ::= [APPLICATION 6] SEQUENCE { +// object LDAPDN, +// changes SEQUENCE OF change SEQUENCE { +// operation ENUMERATED { +// add (0), +// delete (1), +// replace (2), +// ... }, +// modification PartialAttribute } } +// +// PartialAttribute ::= SEQUENCE { +// type AttributeDescription, +// vals SET OF value AttributeValue } +// +// AttributeDescription ::= LDAPString +// -- Constrained to +// -- [RFC4512] +// +// AttributeValue ::= OCTET STRING +// + +package ldap + +import ( + "errors" + "log" + + "github.com/vanackere/asn1-ber" +) + +const ( + AddAttribute = 0 + DeleteAttribute = 1 + ReplaceAttribute = 2 +) + +type PartialAttribute struct { + attrType string + attrVals []string +} + +func (p *PartialAttribute) encode() *ber.Packet { + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PartialAttribute") + seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, p.attrType, "Type")) + set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") + for _, value := range p.attrVals { + set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) + } + seq.AppendChild(set) + return seq +} + +type ModifyRequest struct { + dn string + addAttributes []PartialAttribute + deleteAttributes []PartialAttribute + replaceAttributes []PartialAttribute +} + +func (m *ModifyRequest) Add(attrType string, attrVals []string) { + m.addAttributes = append(m.addAttributes, PartialAttribute{attrType: attrType, attrVals: attrVals}) +} + +func (m *ModifyRequest) Delete(attrType string, attrVals []string) { + m.deleteAttributes = append(m.deleteAttributes, PartialAttribute{attrType: attrType, attrVals: attrVals}) +} + +func (m *ModifyRequest) Replace(attrType string, attrVals []string) { + m.replaceAttributes = append(m.replaceAttributes, PartialAttribute{attrType: attrType, attrVals: attrVals}) +} + +func (m ModifyRequest) encode() *ber.Packet { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyRequest, nil, "Modify Request") + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, m.dn, "DN")) + changes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Changes") + for _, attribute := range m.addAttributes { + change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") + change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(AddAttribute), "Operation")) + change.AppendChild(attribute.encode()) + changes.AppendChild(change) + } + for _, attribute := range m.deleteAttributes { + change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") + change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(DeleteAttribute), "Operation")) + change.AppendChild(attribute.encode()) + changes.AppendChild(change) + } + for _, attribute := range m.replaceAttributes { + change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") + change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(ReplaceAttribute), "Operation")) + change.AppendChild(attribute.encode()) + changes.AppendChild(change) + } + request.AppendChild(changes) + return request +} + +func NewModifyRequest( + dn string, +) *ModifyRequest { + return &ModifyRequest{ + dn: dn, + } +} + +func (l *Conn) Modify(modifyRequest *ModifyRequest) error { + messageID := l.nextMessageID() + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(messageID), "MessageID")) + packet.AppendChild(modifyRequest.encode()) + + l.Debug.PrintPacket(packet) + + channel, err := l.sendMessage(packet) + if err != nil { + return err + } + if channel == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not send message")) + } + defer l.finishMessage(messageID) + + l.Debug.Printf("%d: waiting for response", messageID) + packet = <-channel + l.Debug.Printf("%d: got response %p", messageID, packet) + if packet == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationModifyResponse { + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return NewError(resultCode, errors.New(resultDescription)) + } + } else { + log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + } + + l.Debug.Printf("%d: returning", messageID) + return nil +} diff --git a/Godeps/_workspace/src/github.com/vanackere/ldap/search.go b/Godeps/_workspace/src/github.com/vanackere/ldap/search.go new file mode 100644 index 000000000..d37d96f76 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vanackere/ldap/search.go @@ -0,0 +1,350 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// File contains Search functionality +// +// https://tools.ietf.org/html/rfc4511 +// +// SearchRequest ::= [APPLICATION 3] SEQUENCE { +// baseObject LDAPDN, +// scope ENUMERATED { +// baseObject (0), +// singleLevel (1), +// wholeSubtree (2), +// ... }, +// derefAliases ENUMERATED { +// neverDerefAliases (0), +// derefInSearching (1), +// derefFindingBaseObj (2), +// derefAlways (3) }, +// sizeLimit INTEGER (0 .. maxInt), +// timeLimit INTEGER (0 .. maxInt), +// typesOnly BOOLEAN, +// filter Filter, +// attributes AttributeSelection } +// +// AttributeSelection ::= SEQUENCE OF selector LDAPString +// -- The LDAPString is constrained to +// -- in Section 4.5.1.8 +// +// Filter ::= CHOICE { +// and [0] SET SIZE (1..MAX) OF filter Filter, +// or [1] SET SIZE (1..MAX) OF filter Filter, +// not [2] Filter, +// equalityMatch [3] AttributeValueAssertion, +// substrings [4] SubstringFilter, +// greaterOrEqual [5] AttributeValueAssertion, +// lessOrEqual [6] AttributeValueAssertion, +// present [7] AttributeDescription, +// approxMatch [8] AttributeValueAssertion, +// extensibleMatch [9] MatchingRuleAssertion, +// ... } +// +// SubstringFilter ::= SEQUENCE { +// type AttributeDescription, +// substrings SEQUENCE SIZE (1..MAX) OF substring CHOICE { +// initial [0] AssertionValue, -- can occur at most once +// any [1] AssertionValue, +// final [2] AssertionValue } -- can occur at most once +// } +// +// MatchingRuleAssertion ::= SEQUENCE { +// matchingRule [1] MatchingRuleId OPTIONAL, +// type [2] AttributeDescription OPTIONAL, +// matchValue [3] AssertionValue, +// dnAttributes [4] BOOLEAN DEFAULT FALSE } +// +// + +package ldap + +import ( + "errors" + "fmt" + "strings" + + "github.com/vanackere/asn1-ber" +) + +const ( + ScopeBaseObject = 0 + ScopeSingleLevel = 1 + ScopeWholeSubtree = 2 +) + +var ScopeMap = map[int]string{ + ScopeBaseObject: "Base Object", + ScopeSingleLevel: "Single Level", + ScopeWholeSubtree: "Whole Subtree", +} + +const ( + NeverDerefAliases = 0 + DerefInSearching = 1 + DerefFindingBaseObj = 2 + DerefAlways = 3 +) + +var DerefMap = map[int]string{ + NeverDerefAliases: "NeverDerefAliases", + DerefInSearching: "DerefInSearching", + DerefFindingBaseObj: "DerefFindingBaseObj", + DerefAlways: "DerefAlways", +} + +type Entry struct { + DN string + Attributes []*EntryAttribute +} + +func (e *Entry) GetAttributeValues(attribute string) []string { + for _, attr := range e.Attributes { + if attr.Name == attribute { + return attr.Values + } + } + return []string{} +} + +func (e *Entry) GetAttributeValue(attribute string) string { + values := e.GetAttributeValues(attribute) + if len(values) == 0 { + return "" + } + return values[0] +} + +func (e *Entry) Print() { + fmt.Printf("DN: %s\n", e.DN) + for _, attr := range e.Attributes { + attr.Print() + } +} + +func (e *Entry) PrettyPrint(indent int) { + fmt.Printf("%sDN: %s\n", strings.Repeat(" ", indent), e.DN) + for _, attr := range e.Attributes { + attr.PrettyPrint(indent + 2) + } +} + +type EntryAttribute struct { + Name string + Values []string +} + +func (e *EntryAttribute) Print() { + fmt.Printf("%s: %s\n", e.Name, e.Values) +} + +func (e *EntryAttribute) PrettyPrint(indent int) { + fmt.Printf("%s%s: %s\n", strings.Repeat(" ", indent), e.Name, e.Values) +} + +type SearchResult struct { + Entries []*Entry + Referrals []string + Controls []Control +} + +func (s *SearchResult) Print() { + for _, entry := range s.Entries { + entry.Print() + } +} + +func (s *SearchResult) PrettyPrint(indent int) { + for _, entry := range s.Entries { + entry.PrettyPrint(indent) + } +} + +type SearchRequest struct { + BaseDN string + Scope int + DerefAliases int + SizeLimit int + TimeLimit int + TypesOnly bool + Filter string + Attributes []string + Controls []Control +} + +func (s *SearchRequest) encode() (*ber.Packet, error) { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request") + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, s.BaseDN, "Base DN")) + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(s.Scope), "Scope")) + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(s.DerefAliases), "Deref Aliases")) + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(s.SizeLimit), "Size Limit")) + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(s.TimeLimit), "Time Limit")) + request.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, s.TypesOnly, "Types Only")) + // compile and encode filter + filterPacket, err := CompileFilter(s.Filter) + if err != nil { + return nil, err + } + request.AppendChild(filterPacket) + // encode attributes + attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") + for _, attribute := range s.Attributes { + attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) + } + request.AppendChild(attributesPacket) + return request, nil +} + +func NewSearchRequest( + BaseDN string, + Scope, DerefAliases, SizeLimit, TimeLimit int, + TypesOnly bool, + Filter string, + Attributes []string, + Controls []Control, +) *SearchRequest { + return &SearchRequest{ + BaseDN: BaseDN, + Scope: Scope, + DerefAliases: DerefAliases, + SizeLimit: SizeLimit, + TimeLimit: TimeLimit, + TypesOnly: TypesOnly, + Filter: Filter, + Attributes: Attributes, + Controls: Controls, + } +} + +func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) { + if searchRequest.Controls == nil { + searchRequest.Controls = make([]Control, 0) + } + + pagingControl := NewControlPaging(pagingSize) + searchRequest.Controls = append(searchRequest.Controls, pagingControl) + searchResult := new(SearchResult) + for { + result, err := l.Search(searchRequest) + l.Debug.Printf("Looking for Paging Control...") + if err != nil { + return searchResult, err + } + if result == nil { + return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) + } + + for _, entry := range result.Entries { + searchResult.Entries = append(searchResult.Entries, entry) + } + for _, referral := range result.Referrals { + searchResult.Referrals = append(searchResult.Referrals, referral) + } + for _, control := range result.Controls { + searchResult.Controls = append(searchResult.Controls, control) + } + + l.Debug.Printf("Looking for Paging Control...") + pagingResult := FindControl(result.Controls, ControlTypePaging) + if pagingResult == nil { + pagingControl = nil + l.Debug.Printf("Could not find paging control. Breaking...") + break + } + + cookie := pagingResult.(*ControlPaging).Cookie + if len(cookie) == 0 { + pagingControl = nil + l.Debug.Printf("Could not find cookie. Breaking...") + break + } + pagingControl.SetCookie(cookie) + } + + if pagingControl != nil { + l.Debug.Printf("Abandoning Paging...") + pagingControl.PagingSize = 0 + l.Search(searchRequest) + } + + return searchResult, nil +} + +func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { + messageID := l.nextMessageID() + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(messageID), "MessageID")) + // encode search request + encodedSearchRequest, err := searchRequest.encode() + if err != nil { + return nil, err + } + packet.AppendChild(encodedSearchRequest) + // encode search controls + if searchRequest.Controls != nil { + packet.AppendChild(encodeControls(searchRequest.Controls)) + } + + l.Debug.PrintPacket(packet) + + channel, err := l.sendMessage(packet) + if err != nil { + return nil, err + } + if channel == nil { + return nil, NewError(ErrorNetwork, errors.New("ldap: could not send message")) + } + defer l.finishMessage(messageID) + + result := &SearchResult{ + Entries: make([]*Entry, 0), + Referrals: make([]string, 0), + Controls: make([]Control, 0)} + + foundSearchResultDone := false + for !foundSearchResultDone { + l.Debug.Printf("%d: waiting for response", messageID) + packet = <-channel + l.Debug.Printf("%d: got response %p", messageID, packet) + if packet == nil { + return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return nil, err + } + ber.PrintPacket(packet) + } + + switch packet.Children[1].Tag { + case 4: + entry := new(Entry) + entry.DN = packet.Children[1].Children[0].Value.(string) + for _, child := range packet.Children[1].Children[1].Children { + attr := new(EntryAttribute) + attr.Name = child.Children[0].Value.(string) + for _, value := range child.Children[1].Children { + attr.Values = append(attr.Values, value.Value.(string)) + } + entry.Attributes = append(entry.Attributes, attr) + } + result.Entries = append(result.Entries, entry) + case 5: + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return result, NewError(resultCode, errors.New(resultDescription)) + } + if len(packet.Children) == 3 { + for _, child := range packet.Children[2].Children { + result.Controls = append(result.Controls, DecodeControl(child)) + } + } + foundSearchResultDone = true + case 19: + result.Referrals = append(result.Referrals, packet.Children[1].Children[0].Value.(string)) + } + } + l.Debug.Printf("%d: returning", messageID) + return result, nil +} diff --git a/builtin/credential/ldap/backend.go b/builtin/credential/ldap/backend.go new file mode 100644 index 000000000..4cfe8be5e --- /dev/null +++ b/builtin/credential/ldap/backend.go @@ -0,0 +1,108 @@ +package ldap + +import ( + "fmt" + "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "github.com/vanackere/ldap" +) + +func Factory(map[string]string) (logical.Backend, error) { + return Backend(), nil +} + +func Backend() *framework.Backend { + var b backend + b.Backend = &framework.Backend{ + Help: backendHelp, + + PathsSpecial: &logical.Paths{ + Root: []string{ + "config", + "groups/*", + }, + + Unauthenticated: []string{ + "login/*", + }, + }, + + Paths: append([]*framework.Path{ + pathLogin(&b), + pathConfig(&b), + pathGroups(&b), + }), + + AuthRenew: b.pathLoginRenew, + } + + return b.Backend +} + +type backend struct { + *framework.Backend +} + +func (b *backend) Login(req *logical.Request, username string, password string) ([]string, *logical.Response, error) { + + cfg, err := b.Config(req) + if err != nil { + return nil, nil, err + } + if cfg == nil { + return nil, logical.ErrorResponse("ldap backend not configured"), nil + } + + c, err := cfg.DialLDAP() + if err != nil { + return nil, logical.ErrorResponse(err.Error()), nil + } + + // Try to authenticate to the server using the provided credentials + binddn := fmt.Sprintf("%s=%s,%s", cfg.UserAttr, username, cfg.UserDN) + if err = c.Bind(binddn, password); err != nil { + return nil, logical.ErrorResponse(fmt.Sprintf("LDAP bind failed: %v", err)), nil + } + + // Enumerate all groups the user is member of. The search filter should + // work with both openldap and MS AD standard schemas. + sresult, err := c.Search(&ldap.SearchRequest{ + BaseDN: cfg.GroupDN, + Scope: 2, // subtree + Filter: fmt.Sprintf("(|(memberUid=%s)(member=%s)(uniqueMember=%s))", username, binddn, binddn), + }) + if err != nil { + return nil, logical.ErrorResponse(fmt.Sprintf("LDAP search failed: %v", err)), nil + } + + var allgroups []string + var policies []string + for _, e := range sresult.Entries { + // Expected syntax for group DN: cn=groupanem,ou=Group,dc=example,dc=com + dn := strings.Split(e.DN, ",") + gname := strings.SplitN(dn[0], "=", 2)[1] + allgroups = append(allgroups, gname) + group, err := b.Group(req.Storage, gname) + if err == nil && group != nil { + policies = append(policies, group.Policies...) + } + } + + if len(policies) == 0 { + return nil, logical.ErrorResponse("user is not member of any authorized group"), nil + } + + return policies, nil, nil +} + +const backendHelp = ` +The "ldap" credential provider allows authentication querying +a LDAP server, checking username and password, and associating groups +to set of policies. + +Configuration of the server is done through the "config" and "groups" +endpoints by a user with root access. Authentication is then done +by suppying the two fields for "login". +` diff --git a/builtin/credential/ldap/backend_test.go b/builtin/credential/ldap/backend_test.go new file mode 100644 index 000000000..ffdc6bfb9 --- /dev/null +++ b/builtin/credential/ldap/backend_test.go @@ -0,0 +1,110 @@ +package ldap + +import ( + "fmt" + "testing" + + "github.com/hashicorp/vault/logical" + logicaltest "github.com/hashicorp/vault/logical/testing" + "github.com/mitchellh/mapstructure" +) + +func TestBackend_basic(t *testing.T) { + b := Backend() + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepConfigUrl(t), + testAccStepGroup(t, "scientists", "foo"), + testAccStepLogin(t, "tesla", "password"), + }, + }) +} + +func TestBackend_groupCrud(t *testing.T) { + b := Backend() + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepGroup(t, "g1", "foo"), + testAccStepReadGroup(t, "g1", "foo"), + testAccStepDeleteGroup(t, "g1"), + testAccStepReadGroup(t, "g1", ""), + }, + }) +} + +func testAccStepConfigUrl(t *testing.T) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "config", + Data: map[string]interface{}{ + // Online LDAP test server + // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ + "url": "ldap://ldap.forumsys.com", + "userattr": "uid", + "userdn": "dc=example,dc=com", + "groupdn": "dc=example,dc=com", + }, + } +} + +func testAccStepGroup(t *testing.T, group string, policies string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "groups/" + group, + Data: map[string]interface{}{ + "policies": policies, + }, + } +} + +func testAccStepReadGroup(t *testing.T, group string, policies string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "groups/" + group, + Check: func(resp *logical.Response) error { + if resp == nil { + if policies == "" { + return nil + } + return fmt.Errorf("bad: %#v", resp) + } + + var d struct { + Policies string `mapstructure:"policies"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + + if d.Policies != policies { + return fmt.Errorf("bad: %#v", resp) + } + + return nil + }, + } +} + +func testAccStepDeleteGroup(t *testing.T, group string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.DeleteOperation, + Path: "groups/" + group, + } +} + +func testAccStepLogin(t *testing.T, user string, pass string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "login/" + user, + Data: map[string]interface{}{ + "password": pass, + }, + Unauthenticated: true, + + Check: logicaltest.TestCheckAuth([]string{"foo"}), + } +} diff --git a/builtin/credential/ldap/cli.go b/builtin/credential/ldap/cli.go new file mode 100644 index 000000000..38d6c2ef4 --- /dev/null +++ b/builtin/credential/ldap/cli.go @@ -0,0 +1,61 @@ +package ldap + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/vault/api" + pwd "github.com/hashicorp/vault/helper/password" +) + +type CLIHandler struct{} + +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { + mount, ok := m["mount"] + if !ok { + mount = "ldap" + } + + username, ok := m["username"] + if !ok { + return "", fmt.Errorf("'username' var must be set") + } + password, ok := m["password"] + if !ok { + fmt.Printf("Password (will be hidden): ") + var err error + password, err = pwd.Read(os.Stdin) + fmt.Println() + if err != nil { + return "", err + } + } + + path := fmt.Sprintf("auth/%s/login/%s", mount, username) + secret, err := c.Logical().Write(path, map[string]interface{}{ + "password": password, + }) + if err != nil { + return "", err + } + if secret == nil { + return "", fmt.Errorf("empty response from credential provider") + } + + return secret.Auth.ClientToken, nil +} + +func (h *CLIHandler) Help() string { + help := ` +The LDAP credential provider allows you to authenticate with LDAP. +To use it, first configure it through the "config" endpoint, and then +login by specifying username and password. If password is not provided +on the command line, it will be read from stdin. + + Example: vault auth -method=ldap username=john + + ` + + return strings.TrimSpace(help) +} diff --git a/builtin/credential/ldap/path_config.go b/builtin/credential/ldap/path_config.go new file mode 100644 index 000000000..bfd4d10bc --- /dev/null +++ b/builtin/credential/ldap/path_config.go @@ -0,0 +1,180 @@ +package ldap + +import ( + "fmt" + "net" + "net/url" + "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "github.com/vanackere/ldap" +) + +func pathConfig(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `config`, + Fields: map[string]*framework.FieldSchema{ + "url": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "ldap URL to connect to (default: ldap://127.0.0.1)", + }, + "userdn": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "LDAP domain to use for users (eg: ou=People,dc=example,dc=org)", + }, + "groupdn": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "LDAP domain to use for groups (eg: ou=Groups,dc=example,dc=org)", + }, + "userattr": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Attribute used for users (default: cn)", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathConfigRead, + logical.WriteOperation: b.pathConfigWrite, + }, + + HelpSynopsis: pathConfigHelpSyn, + HelpDescription: pathConfigHelpDesc, + } +} + +func (b *backend) Config(req *logical.Request) (*ConfigEntry, error) { + entry, err := req.Storage.Get("config") + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + var result ConfigEntry + result.SetDefaults() + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + return &result, nil +} + +func (b *backend) pathConfigRead( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + + cfg, err := b.Config(req) + if err != nil { + return nil, err + } + if cfg == nil { + return nil, nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + "url": cfg.Url, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "userattr": cfg.UserAttr, + }, + }, nil +} + +func (b *backend) pathConfigWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + + cfg := &ConfigEntry{} + url := d.Get("url").(string) + if url != "" { + cfg.Url = strings.ToLower(url) + } + userattr := d.Get("userattr").(string) + if userattr != "" { + cfg.UserAttr = strings.ToLower(userattr) + } + userdn := d.Get("userdn").(string) + if userdn != "" { + cfg.UserDN = userdn + } + groupdn := d.Get("groupdn").(string) + if groupdn != "" { + cfg.GroupDN = groupdn + } + + // Try to connect to the LDAP server, to validate the URL configuration + // We can also check the URL at this stage, as anything else would probably + // require authentication. + conn, cerr := cfg.DialLDAP() + if cerr != nil { + return logical.ErrorResponse(cerr.Error()), nil + } + conn.Close() + + entry, err := logical.StorageEntryJSON("config", cfg) + if err != nil { + return nil, err + } + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +type ConfigEntry struct { + Url string + UserDN string + GroupDN string + UserAttr string +} + +func (c *ConfigEntry) DialLDAP() (*ldap.Conn, error) { + + u, err := url.Parse(c.Url) + if err != nil { + return nil, err + } + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + host = u.Host + } + + var conn *ldap.Conn + switch u.Scheme { + case "ldap": + if port == "" { + port = "389" + } + conn, err = ldap.Dial("tcp", host+":"+port) + case "ldaps": + if port == "" { + port = "636" + } + conn, err = ldap.DialTLS("tcp", host+":"+port, nil) + default: + return nil, fmt.Errorf("invalid LDAP scheme") + } + if err != nil { + return nil, fmt.Errorf("cannot connect to LDAP: %v", err) + } + + return conn, nil +} + +func (c *ConfigEntry) SetDefaults() { + c.Url = "ldap://127.0.0.1" + c.UserAttr = "cn" +} + +const pathConfigHelpSyn = ` +Configure the LDAP server to connect to. +` + +const pathConfigHelpDesc = ` +This endpoint allows you to configure the LDAP server to connect to, and give +basic information of the schema of that server. + +The LDAP URL can use either the "ldap://" or "ldaps://" schema. In the former +case, an unencrypted connection will be done, with default port 389; in the latter +case, a SSL connection will be done, with default port 636. +` diff --git a/builtin/credential/ldap/path_groups.go b/builtin/credential/ldap/path_groups.go new file mode 100644 index 000000000..670d58c84 --- /dev/null +++ b/builtin/credential/ldap/path_groups.go @@ -0,0 +1,118 @@ +package ldap + +import ( + "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathGroups(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `groups/(?P\w+)`, + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the LDAP group.", + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Comma-separated list of policies associated to the group.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.DeleteOperation: b.pathGroupDelete, + logical.ReadOperation: b.pathGroupRead, + logical.WriteOperation: b.pathGroupWrite, + }, + + HelpSynopsis: pathGroupHelpSyn, + HelpDescription: pathGroupHelpDesc, + } +} + +func (b *backend) Group(s logical.Storage, n string) (*GroupEntry, error) { + entry, err := s.Get("group/" + n) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result GroupEntry + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (b *backend) pathGroupDelete( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + err := req.Storage.Delete("group/" + d.Get("name").(string)) + if err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathGroupRead( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + group, err := b.Group(req.Storage, d.Get("name").(string)) + if err != nil { + return nil, err + } + if group == nil { + return nil, nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + "policies": strings.Join(group.Policies, ","), + }, + }, nil +} + +func (b *backend) pathGroupWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + policies := strings.Split(d.Get("policies").(string), ",") + for i, p := range policies { + policies[i] = strings.TrimSpace(p) + } + + // Store it + entry, err := logical.StorageEntryJSON("group/"+name, &GroupEntry{ + Policies: policies, + }) + if err != nil { + return nil, err + } + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +type GroupEntry struct { + Policies []string +} + +const pathGroupHelpSyn = ` +Manage users allowed to authenticate. +` + +const pathGroupHelpDesc = ` +This endpoint allows you to create, read, update, and delete configuration +for LDAP groups that are allowed to authenticate, and associate policies to +them. + +Deleting a group will not revoke auth for prior authenticated users in that +group. To do this, do a revoke on "login/" for +the usernames you want revoked. +` diff --git a/builtin/credential/ldap/path_login.go b/builtin/credential/ldap/path_login.go new file mode 100644 index 000000000..4dbbfa17c --- /dev/null +++ b/builtin/credential/ldap/path_login.go @@ -0,0 +1,89 @@ +package ldap + +import ( + "sort" + "strings" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathLogin(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `login/(?P\w+)`, + Fields: map[string]*framework.FieldSchema{ + "username": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "DN (distinguished name) to be used for login.", + }, + + "password": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Password for this user.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathLogin, + }, + + HelpSynopsis: pathLoginSyn, + HelpDescription: pathLoginDesc, + } +} + +func (b *backend) pathLogin( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + username := d.Get("username").(string) + password := d.Get("password").(string) + + policies, resp, err := b.Login(req, username, password) + if len(policies) == 0 { + return resp, err + } + + sort.Strings(policies) + + return &logical.Response{ + Auth: &logical.Auth{ + Policies: policies, + Metadata: map[string]string{ + "username": username, + "policies": strings.Join(policies, ","), + }, + InternalData: map[string]interface{}{ + "password": password, + }, + DisplayName: username, + }, + }, nil +} + +func (b *backend) pathLoginRenew( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + + username := req.Auth.Metadata["username"] + password := req.Auth.InternalData["password"].(string) + prevpolicies := req.Auth.Metadata["policies"] + + policies, resp, err := b.Login(req, username, password) + if len(policies) == 0 { + return resp, err + } + + sort.Strings(policies) + if strings.Join(policies, ",") != prevpolicies { + return logical.ErrorResponse("policies have changed, revoking login"), nil + } + + return framework.LeaseExtend(1*time.Hour, 0)(req, d) +} + +const pathLoginSyn = ` +Log in with a username and password. +` + +const pathLoginDesc = ` +This endpoint authenticates using a username and password. +` diff --git a/cli/commands.go b/cli/commands.go index d876241e6..f4d1832aa 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -9,6 +9,7 @@ import ( credAppId "github.com/hashicorp/vault/builtin/credential/app-id" credCert "github.com/hashicorp/vault/builtin/credential/cert" credGitHub "github.com/hashicorp/vault/builtin/credential/github" + credLdap "github.com/hashicorp/vault/builtin/credential/ldap" credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" "github.com/hashicorp/vault/builtin/logical/aws" @@ -58,6 +59,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { "app-id": credAppId.Factory, "github": credGitHub.Factory, "userpass": credUserpass.Factory, + "ldap": credLdap.Factory, }, LogicalBackends: map[string]logical.Factory{ "aws": aws.Factory, @@ -81,6 +83,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { Handlers: map[string]command.AuthHandler{ "github": &credGitHub.CLIHandler{}, "userpass": &credUserpass.CLIHandler{}, + "ldap": &credLdap.CLIHandler{}, }, }, nil },