diff --git a/ldif/apply.go b/ldif/apply.go new file mode 100644 index 00000000..917c0612 --- /dev/null +++ b/ldif/apply.go @@ -0,0 +1,67 @@ +package ldif + +import ( + "fmt" + "log" + + "gopkg.in/ldap.v2" +) + +// Apply sends the LDIF entries to the server and does the changes as +// given by the entries. +// +// All *ldap.Entry are converted to an *ldap.AddRequest. +// +// By default, it returns on the first error. To continue with applying the +// LDIF, set the continueOnErr argument to true - in this case the errors +// are logged with log.Printf() +func (l *LDIF) Apply(conn ldap.Client, continueOnErr bool) error { + for _, entry := range l.Entries { + switch { + case entry.Entry != nil: + add := ldap.NewAddRequest(entry.Entry.DN) + for _, attr := range entry.Entry.Attributes { + add.Attribute(attr.Name, attr.Values) + } + entry.Add = add + fallthrough + case entry.Add != nil: + if err := conn.Add(entry.Add); err != nil { + if continueOnErr { + log.Printf("ERROR: Failed to add %s: %s", entry.Add.DN, err) + continue + } + return fmt.Errorf("failed to add %s: %s", entry.Add.DN, err) + } + + case entry.Del != nil: + if err := conn.Del(entry.Del); err != nil { + if continueOnErr { + log.Printf("ERROR: Failed to delete %s: %s", entry.Del.DN, err) + continue + } + return fmt.Errorf("failed to delete %s: %s", entry.Del.DN, err) + } + + case entry.Modify != nil: + if err := conn.Modify(entry.Modify); err != nil { + if continueOnErr { + log.Printf("ERROR: Failed to modify %s: %s", entry.Modify.DN, err) + continue + } + return fmt.Errorf("failed to modify %s: %s", entry.Modify.DN, err) + } + /* + case entry.ModifyDN != nil: + if err := conn.ModifyDN(entry.ModifyDN); err != nil { + if continueOnErr { + log.Printf("ERROR: Failed to modify dn %s: %s", entry.ModifyDN.DN, err) + continue + } + return fmt.Errorf("failed to modify dn %s: %s", entry.ModifyDN.DN, err) + } + */ + } + } + return nil +} diff --git a/ldif/ldif.go b/ldif/ldif.go new file mode 100644 index 00000000..ee69bc14 --- /dev/null +++ b/ldif/ldif.go @@ -0,0 +1,335 @@ +// Package ldif contains a basic LDIF parser (RFC 2849). This one currently +// just supports LDIFs like they are generated by tools like ldapsearch(1) +// slapcat(8). Change records are not supported while unmarshalling. +// For marshalling support for mod(r)dn is missing. +// +// Controls are not supported in both modes. +// +// URL schemes in an LDIF like +// jpegPhoto;binary:< file:///usr/share/photos/someone.jpg +// are only supported for the "file" scheme like in the example above +package ldif + +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "io/ioutil" + "net/url" + + "gopkg.in/ldap.v2" + // "os" + "strconv" + "strings" +) + +// Entry is one entry in the LDIF +type Entry struct { + Entry *ldap.Entry + Add *ldap.AddRequest + Del *ldap.DelRequest + Modify *ldap.ModifyRequest + //ModDN *ldap.ModifyDNRequest +} + +// The LDIF struct is used for parsing an LDIF +type LDIF struct { + Entries []*Entry + Version int + changeType string + FoldWidth int + firstEntry bool +} + +// The ParseError holds the error message and the line in the ldif +// where the error occurred. +type ParseError struct { + Line int + Message string +} + +// Error implements the error interface +func (e *ParseError) Error() string { + return fmt.Sprintf("Error in line %d: %s", e.Line, e.Message) +} + +var cr byte = '\x0D' +var lf byte = '\x0A' +var sep = string([]byte{cr, lf}) +var comment byte = '#' +var space byte = ' ' +var spaces = string(space) + +// Parse wraps Unmarshal to parse an LDIF from a string +func Parse(str string) (l *LDIF, err error) { + buf := bytes.NewBuffer([]byte(str)) + l = &LDIF{} + err = Unmarshal(buf, l) + return +} + +// Unmarshal parses the LDIF from the given io.Reader into the LDIF struct. +// The caller is responsible for closing the io.Reader if that is +// needed. +func Unmarshal(r io.Reader, l *LDIF) (err error) { + if r == nil { + return &ParseError{Line: 0, Message: "No reader present"} + } + curLine := 0 + l.Version = 0 + l.changeType = "" + isComment := false + + reader := bufio.NewReader(r) + + var lines []string + var line, nextLine string + l.firstEntry = true + + for { + curLine++ + nextLine, err = reader.ReadString(lf) + nextLine = strings.TrimRight(nextLine, sep) + + switch err { + case nil, io.EOF: + switch len(nextLine) { + case 0: + if len(line) == 0 && err == io.EOF { + return nil + } + if len(line) == 0 && len(lines) == 0 { + continue + } + lines = append(lines, line) + entry, perr := l.parseEntry(lines) + if perr != nil { + return &ParseError{Line: curLine, Message: perr.Error()} + } + l.Entries = append(l.Entries, entry) + line = "" + lines = []string{} + if err == io.EOF { + return nil + } + default: + switch nextLine[0] { + case comment: + isComment = true + continue + + case space: + if isComment { + continue + } + line += nextLine[1:] + continue + + default: + isComment = false + if len(line) != 0 { + lines = append(lines, line) + } + line = nextLine + continue + } + } + default: + return &ParseError{Line: curLine, Message: err.Error()} + } + } +} + +func (l *LDIF) parseEntry(lines []string) (entry *Entry, err error) { + if len(lines) == 0 { + return nil, errors.New("empty entry?") + } + + if l.firstEntry && strings.HasPrefix(lines[0], "version:") { + l.firstEntry = false + line := strings.TrimLeft(lines[0][8:], spaces) + if l.Version, err = strconv.Atoi(line); err != nil { + return nil, err + } + + if l.Version != 1 { + return nil, errors.New("Invalid version spec " + string(line)) + } + + l.Version = 1 + if len(lines) == 1 { + return nil, nil + } + lines = lines[1:] + } + l.firstEntry = false + + if len(lines) == 0 { + return nil, nil + } + + if !strings.HasPrefix(lines[0], "dn:") { + return nil, errors.New("Missing dn:") + } + _, val, err := l.parseLine(lines[0]) + if err != nil { + return nil, err + } + dn := val + + if len(lines) == 1 { + return nil, errors.New("only a dn: line") + } + + lines = lines[1:] + if strings.HasPrefix(lines[0], "changetype:") { + _, val, err := l.parseLine(lines[0]) + if err != nil { + return nil, err + } + l.changeType = val + if len(lines) > 1 { + lines = lines[1:] + } + } + if l.changeType != "" { + return nil, errors.New("change records not supported") + } + + attrs := make(map[string][]string) + for i := 0; i < len(lines); i++ { + attr, val, err := l.parseLine(lines[i]) + if err != nil { + return nil, err + } + attrs[attr] = append(attrs[attr], val) + } + return &Entry{ + Entry: ldap.NewEntry(dn, attrs), + }, nil +} + +func (l *LDIF) parseLine(line string) (attr, val string, err error) { + off := 0 + for len(line) > off && line[off] != ':' { + off++ + if off >= len(line) { + err = errors.New("Missing : in line") + return + } + } + if off == len(line) { + err = errors.New("Missing : in line") + return + } + + if off > len(line)-2 { + err = errors.New("empty value") + // FIXME: this is allowed for some attributes + return + } + + attr = line[0:off] + if err = validAttr(attr); err != nil { + attr = "" + val = "" + return + } + + switch line[off+1] { + case ':': + var n int + value := strings.TrimLeft(line[off+2:], spaces) + dec := make([]byte, base64.StdEncoding.DecodedLen(len([]byte(value)))) + n, err = base64.StdEncoding.Decode(dec, []byte(value)) + if err != nil { + return + } + val = string(dec[:n]) + + case '<': + var u *url.URL + var data []byte + val = strings.TrimLeft(line[off+2:], spaces) + u, err = url.Parse(val) + if err != nil { + err = fmt.Errorf("failed to parse URL: %s", err) + return + } + if u.Scheme != "file" { + err = fmt.Errorf("unsupported URL scheme %s", u.Scheme) + return + } + data, err = ioutil.ReadFile(u.Path) + if err != nil { + err = fmt.Errorf("failed to read %s: %s", u.Path, err) + return + } + val = string(data) // FIXME: safe? + + default: + val = strings.TrimLeft(line[off+1:], spaces) + } + + return +} + +func validOID(oid string) error { + lastDot := true + for _, c := range oid { + switch { + case c == '.' && lastDot: + return errors.New("OID with at least 2 consecutive dots") + case c == '.': + lastDot = true + case c >= '0' && c <= '9': + lastDot = false + default: + return errors.New("Invalid character in OID") + } + } + return nil +} + +func validAttr(attr string) error { + if len(attr) == 0 { + return errors.New("empty attribute name") + } + switch { + case attr[0] >= 'A' && attr[0] <= 'Z': + // A-Z + case attr[0] >= 'a' && attr[0] <= 'z': + // a-z + default: + if attr[0] >= '0' && attr[0] <= '9' { + return validOID(attr) + } + return errors.New("invalid first character in attribute") + } + for i := 1; i < len(attr); i++ { + c := attr[i] + switch { + case c >= '0' && c <= '9': + case c >= 'A' && c <= 'Z': + case c >= 'a' && c <= 'z': + case c == '-': + case c == ';': + default: + return errors.New("invalid character in attribute name") + } + } + return nil +} + +// AllEntries returns all *ldap.Entries in the LDIF +func (l *LDIF) AllEntries() (entries []*ldap.Entry) { + for _, entry := range l.Entries { + if entry.Entry != nil { + entries = append(entries, entry.Entry) + } + } + return entries +} diff --git a/ldif/ldif_test.go b/ldif/ldif_test.go new file mode 100644 index 00000000..3ce25a7d --- /dev/null +++ b/ldif/ldif_test.go @@ -0,0 +1,259 @@ +package ldif_test + +import ( + "io/ioutil" + "os" + "testing" + + "gopkg.in/ldap.v2/ldif" +) + +var ldifRFC2849Example = `version: 1 +dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Barbara Jensen +cn: Barbara J Jensen +cn: Babs Jensen +sn: Jensen +uid: bjensen +telephonenumber: +1 408 555 1212 +description: A big sailing fan. + +dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Bjorn Jensen +sn: Jensen +telephonenumber: +1 408 555 1212 +` + +func TestLDIFParseRFC2849Example(t *testing.T) { + l, err := ldif.Parse(ldifRFC2849Example) + if err != nil { + t.Errorf("Failed to parse RFC 2849 example: %s", err) + } + if l.Entries[1].Entry.GetAttributeValues("sn")[0] != "Jensen" { + t.Errorf("RFC 2849 example: empty 'sn' in second entry") + } +} + +var ldifEmpty = `dn: uid=someone,dc=example,dc=org +cn: +cn: Some User +` + +func TestLDIFParseEmptyAttr(t *testing.T) { + _, err := ldif.Parse(ldifEmpty) + if err == nil { + t.Errorf("Did not fail to parse empty attribute") + } +} + +var ldifMissingDN = `objectclass: top +cn: Some User +` + +func TestLDIFParseMissingDN(t *testing.T) { + _, err := ldif.Parse(ldifMissingDN) + if err == nil { + t.Errorf("Did not fail to parse missing DN attribute") + } +} + +var ldifContinuation = `dn: uid=someone,dc=example,dc=org +sn: Some + One +cn: Someone +` + +func TestLDIFContinuation(t *testing.T) { + l, err := ldif.Parse(ldifContinuation) + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } + e := l.Entries[0] + if e.Entry.GetAttributeValues("sn")[0] != "Some One" { + t.Errorf("Value of continuation line wrong") + } +} + +var ldifBase64 = `dn: uid=someone,dc=example,dc=org +sn:: U29tZSBPbmU= +` + +func TestLDIFBase64(t *testing.T) { + l, err := ldif.Parse(ldifBase64) + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } + + e := l.Entries[0] + val := e.Entry.GetAttributeValues("sn")[0] + cmp := "Some One" + if val != cmp { + t.Errorf("Value of base64 value wrong: >%v< >%v<", []byte(val), []byte(cmp)) + } +} + +var ldifBase64Broken = `dn: uid=someone,dc=example,dc=org +sn:: XXX-U29tZSBPbmU= +` + +func TestLDIFBase64Broken(t *testing.T) { + _, err := ldif.Parse(ldifBase64Broken) + if err == nil { + t.Errorf("Did not failed to parse broken base64") + } +} + +var ldifTrailingBlank = `dn: uid=someone,dc=example,dc=org +sn:: U29tZSBPbmU= + +` + +func TestLDIFTrailingBlank(t *testing.T) { + _, err := ldif.Parse(ldifTrailingBlank) + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } +} + +var ldifComments = `dn: uid=someone,dc=example,dc=org +# a comment + continued comment +sn: someone +` + +func TestLDIFComments(t *testing.T) { + l, err := ldif.Parse(ldifComments) + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } + if l.Entries[0].Entry.GetAttributeValues("sn")[0] != "someone" { + t.Errorf("No sn attribute") + } +} + +var ldifNoSpace = `dn:uid=someone,dc=example,dc=org +sn:someone +` + +func TestLDIFNoSpace(t *testing.T) { + l, err := ldif.Parse(ldifNoSpace) + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } + if l.Entries[0].Entry.GetAttributeValues("sn")[0] != "someone" { + t.Errorf("No/wrong sn attribute: '%s'", l.Entries[0].Entry.GetAttributeValues("sn")[0]) + } +} + +var ldifMultiSpace = `dn: uid=someone,dc=example,dc=org +sn: someone +` + +func TestLDIFMultiSpace(t *testing.T) { + l, err := ldif.Parse(ldifMultiSpace) + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } + if l.Entries[0].Entry.GetAttributeValues("sn")[0] != "someone" { + t.Errorf("No/wrong sn attribute: '%s'", l.Entries[0].Entry.GetAttributeValues("sn")[0]) + } +} + +func TestLDIFURL(t *testing.T) { + f, err := ioutil.TempFile("", "ldifurl") + if err != nil { + t.Errorf("Failed to create temp file: %s", err) + } + defer os.Remove(f.Name()) + f.Write([]byte("TEST\n")) + f.Sync() + l, err := ldif.Parse("dn: uid=someone,dc=example,dc=org\ndescription:< file://" + f.Name() + "\n") + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } + if l.Entries[0].Entry.GetAttributeValues("description")[0] != "TEST\n" { + t.Errorf("wrong file?") + } +} + +var ldifMultiBlankLines = `# Organization Units +dn: ou=users,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: users + + +# searches for above empty line for dn but fails and errors out in this PR +# Even though this is a valid LDIF file for ldapadd +dn: ou=groups,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: groups +` + +func TestLDIFMultiBlankLines(t *testing.T) { + l, err := ldif.Parse(ldifMultiBlankLines) + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } + ou := l.Entries[1].Entry.GetAttributeValue("ou") + if ou != "groups" { + t.Errorf("wrong ou in second entry: %s", ou) + } +} + +var ldifLeadingTrailingBlankLines = ` + +# Organization Units +dn: ou=users,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: users + + +` + +func TestLDIFLeadingTrailingBlankLines(t *testing.T) { + l, err := ldif.Parse(ldifLeadingTrailingBlankLines) + if err != nil { + t.Errorf("Failed to parse LDIF: %s", err) + } + ou := l.Entries[0].Entry.GetAttributeValue("ou") + if ou != "users" { + t.Errorf("wrong ou in entry: %s", ou) + } +} + +var ldifVersionOnSecond = `dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Barbara Jensen +cn: Barbara J Jensen +cn: Babs Jensen +sn: Jensen +uid: bjensen +telephonenumber: +1 408 555 1212 +description: A big sailing fan. + +version: 1 +dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Bjorn Jensen +sn: Jensen +telephonenumber: +1 408 555 1212 +` + +func TestLDIFVersionOnSecond(t *testing.T) { + if _, err := ldif.Parse(ldifVersionOnSecond); err == nil { + t.Errorf("did not fail to parse LDIF") + } +} diff --git a/ldif/marshal.go b/ldif/marshal.go new file mode 100644 index 00000000..618ac812 --- /dev/null +++ b/ldif/marshal.go @@ -0,0 +1,273 @@ +package ldif + +import ( + "encoding/base64" + "errors" + "fmt" + "gopkg.in/ldap.v2" + "io" +) + +var foldWidth = 76 + +// ErrMixed is the error, that we cannot mix change records and content +// records in one LDIF +var ErrMixed = errors.New("cannot mix change records and content records") + +// Marshal returns an LDIF string from the given LDIF. +// +// The default line lenght is 76 characters. This can be changed by setting +// the fw parameter to something else than 0. +// For a fold width < 0, no folding will be done, with 0, the default is used. +func Marshal(l *LDIF) (data string, err error) { + hasEntry := false + hasChange := false + + if l.Version > 0 { + data = "version: 1\n" + } + + fw := l.FoldWidth + if fw == 0 { + fw = foldWidth + } + + for _, e := range l.Entries { + switch { + case e.Add != nil: + hasChange = true + if hasEntry { + return "", ErrMixed + } + data += foldLine("dn: "+e.Add.DN, fw) + "\n" + data += "changetype: add\n" + for _, add := range e.Add.Attributes { + if len(add.Vals) == 0 { + return "", errors.New("changetype 'add' requires non empty value list") + } + for _, v := range add.Vals { + ev, t := encodeValue(v) + col := ": " + if t { + col = ":: " + } + data += foldLine(add.Type+col+ev, fw) + "\n" + } + } + + case e.Del != nil: + hasChange = true + if hasEntry { + return "", ErrMixed + } + data += foldLine("dn: "+e.Del.DN, fw) + "\n" + data += "changetype: delete\n" + + case e.Modify != nil: + hasChange = true + if hasEntry { + return "", ErrMixed + } + data += foldLine("dn: "+e.Modify.DN, fw) + "\n" + data += "changetype: modify\n" + for _, mod := range e.Modify.AddAttributes { + if len(mod.Vals) == 0 { + return "", errors.New("changetype 'modify', op 'add' requires non empty value list") + } + + data += "add: " + mod.Type + "\n" + for _, v := range mod.Vals { + ev, t := encodeValue(v) + col := ": " + if t { + col = ":: " + } + data += foldLine(mod.Type+col+ev, fw) + "\n" + } + data += "-\n" + } + for _, mod := range e.Modify.DeleteAttributes { + data += "delete: " + mod.Type + "\n" + for _, v := range mod.Vals { + ev, t := encodeValue(v) + col := ": " + if t { + col = ":: " + } + data += foldLine(mod.Type+col+ev, fw) + "\n" + } + data += "-\n" + } + for _, mod := range e.Modify.ReplaceAttributes { + if len(mod.Vals) == 0 { + return "", errors.New("changetype 'modify', op 'replace' requires non empty value list") + } + data += "replace: " + mod.Type + "\n" + for _, v := range mod.Vals { + ev, t := encodeValue(v) + col := ": " + if t { + col = ":: " + } + data += foldLine(mod.Type+col+ev, fw) + "\n" + } + data += "-\n" + } + + /* + case e.ModDN != nil: + hasChange = true + if hasEntry { + return "", ErrMixed + } + changeType := "moddn" + // FIXME: + // if e.ModifyDN.NewSuperior == "" { + // changeType = "modrdn" + // } + delOld := 0 + if e.ModDN.DeleteOldRDN { + delOld = 1 + } + data += foldLine("dn: "+e.ModDN.DN, fw) + "\n" + data += foldLine("changetype: "+changeType) + "\n" + data += foldLine(fmt.Sprintf("deleteoldrdn: %d", delOld)) + "\n" + if e.ModDN.NewSuperior != "" { + data += foldLine("newsuperior: "+e.ModDN.NewSuperior) + "\n" + } + data += "-\n" + */ + default: + hasEntry = true + if hasChange { + return "", ErrMixed + } + data += foldLine("dn: "+e.Entry.DN, fw) + "\n" + for _, av := range e.Entry.Attributes { + for _, v := range av.Values { + ev, t := encodeValue(v) + col := ": " + if t { + col = ":: " + } + data += foldLine(av.Name+col+ev, fw) + "\n" + } + } + } + data += "\n" + } + return data, nil +} + +func encodeValue(value string) (string, bool) { + required := false + for _, r := range value { + if r < ' ' || r > '~' { // ~ = 0x7E, = 0x7F + required = true + break + } + } + if !required { + return value, false + } + return base64.StdEncoding.EncodeToString([]byte(value)), true +} + +func foldLine(line string, fw int) (folded string) { + if fw < 0 { + return line + } + if len(line) <= fw { + return line + } + + folded = line[:fw] + "\n" + line = line[fw:] + + for len(line) > fw-1 { + folded += " " + line[:fw-1] + "\n" + line = line[fw-1:] + } + + if len(line) > 0 { + folded += " " + line + } + return +} + +// Dump writes the given entries to the io.Writer. +// +// The entries argument can be *ldap.Entry or a mix of *ldap.AddRequest, +// *ldap.DelRequest, *ldap.ModifyRequest and *ldap.ModifyDNRequest or slices +// of any of those. +// +// See Marshal() for the fw argument. +func Dump(fh io.Writer, fw int, entries ...interface{}) error { + l, err := ToLDIF(entries...) + if err != nil { + return err + } + l.FoldWidth = fw + str, err := Marshal(l) + if err != nil { + return err + } + _, err = fh.Write([]byte(str)) + return err +} + +// ToLDIF puts the given arguments in an LDIF struct and returns it. +// +// The entries argument can be *ldap.Entry or a mix of *ldap.AddRequest, +// *ldap.DelRequest, *ldap.ModifyRequest and *ldap.ModifyDNRequest or slices +// of any of those. +func ToLDIF(entries ...interface{}) (*LDIF, error) { + l := &LDIF{} + for _, e := range entries { + switch e.(type) { + case []*ldap.Entry: + for _, en := range e.([]*ldap.Entry) { + l.Entries = append(l.Entries, &Entry{Entry: en}) + } + + case *ldap.Entry: + l.Entries = append(l.Entries, &Entry{Entry: e.(*ldap.Entry)}) + + case []*ldap.AddRequest: + for _, en := range e.([]*ldap.AddRequest) { + l.Entries = append(l.Entries, &Entry{Add: en}) + } + + case *ldap.AddRequest: + l.Entries = append(l.Entries, &Entry{Add: e.(*ldap.AddRequest)}) + + case []*ldap.DelRequest: + for _, en := range e.([]*ldap.DelRequest) { + l.Entries = append(l.Entries, &Entry{Del: en}) + } + + case *ldap.DelRequest: + l.Entries = append(l.Entries, &Entry{Del: e.(*ldap.DelRequest)}) + + case []*ldap.ModifyRequest: + for _, en := range e.([]*ldap.ModifyRequest) { + l.Entries = append(l.Entries, &Entry{Modify: en}) + } + case *ldap.ModifyRequest: + l.Entries = append(l.Entries, &Entry{Modify: e.(*ldap.ModifyRequest)}) + + /* + case []*ldap.ModifyDNRequest: + for _, en := range e.([]*ldap.ModifyDNRequest) { + l.Entries = append(l.Entries, &Entry{ModifyDN: en}) + } + case *ldap.ModifyDNRequest: + l.Entries = append(l.Entries, &Entry{ModDN: e.(*ldap.ModifyDNRequest)}) + */ + + default: + return nil, fmt.Errorf("unsupported type %T", e) + } + } + return l, nil +} diff --git a/ldif/marshal_test.go b/ldif/marshal_test.go new file mode 100644 index 00000000..8b9e5b0e --- /dev/null +++ b/ldif/marshal_test.go @@ -0,0 +1,272 @@ +package ldif_test + +import ( + "bytes" + "gopkg.in/ldap.v2" + "gopkg.in/ldap.v2/ldif" + "testing" +) + +var personLDIF = `dn: uid=someone,ou=people,dc=example,dc=org +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +uid: someone +cn: Someone +mail: someone@example.org + +` + +var ouLDIF = `dn: ou=people,dc=example,dc=org +objectClass: top +objectClass: organizationalUnit +ou: people + +` + +var entries = []*ldap.Entry{ + { + DN: "ou=people,dc=example,dc=org", + Attributes: []*ldap.EntryAttribute{ + { + Name: "objectClass", + Values: []string{ + "top", + "organizationalUnit", + }, + }, + { + Name: "ou", + Values: []string{"people"}, + }, + }, + }, + { + DN: "uid=someone,ou=people,dc=example,dc=org", + Attributes: []*ldap.EntryAttribute{ + { + Name: "objectClass", + Values: []string{ + "top", + "person", + "organizationalPerson", + "inetOrgPerson", + }, + }, + { + Name: "uid", + Values: []string{"someone"}, + }, + { + Name: "cn", + Values: []string{"Someone"}, + }, + { + Name: "mail", + Values: []string{"someone@example.org"}, + }, + }, + }, +} + +func TestMarshalSingleEntry(t *testing.T) { + l := &ldif.LDIF{ + Entries: []*ldif.Entry{ + {Entry: entries[1]}, + }, + } + res, err := ldif.Marshal(l) + if err != nil { + t.Errorf("Failed to marshal entry: %s", err) + } + if res != personLDIF { + t.Errorf("unexpected result: >>%s<<\n", res) + } +} + +func TestMarshalEntries(t *testing.T) { + l := &ldif.LDIF{ + Entries: []*ldif.Entry{ + {Entry: entries[0]}, + {Entry: entries[1]}, + }, + } + res, err := ldif.Marshal(l) + if err != nil { + t.Errorf("Failed to marshal entry: %s", err) + } + if res != ouLDIF+personLDIF { + t.Errorf("unexpected result: >>%s<<\n", res) + } +} + +func TestMarshalB64(t *testing.T) { + entryLDIF := `dn: ou=people,dc=example,dc=org +objectClass: top +objectClass: organizationalUnit +ou: people +description:: VGhlIFBlw7ZwbGUgw5ZyZ2FuaXphdGlvbg== + +` + entry := &ldap.Entry{ + DN: "ou=people,dc=example,dc=org", + Attributes: []*ldap.EntryAttribute{ + { + Name: "objectClass", + Values: []string{ + "top", + "organizationalUnit", + }, + }, + { + Name: "ou", + Values: []string{"people"}, + }, + { + Name: "description", + Values: []string{"The Peöple Örganization"}, + }, + }, + } + l := &ldif.LDIF{ + Entries: []*ldif.Entry{ + {Entry: entry}, + }, + } + res, err := ldif.Marshal(l) + if err != nil { + t.Errorf("Failed to marshal entry: %s", err) + } + if res != entryLDIF { + t.Errorf("unexpected result: >>%s<<\n", res) + } +} + +func TestMarshalMod(t *testing.T) { + modLDIF := `dn: uid=someone,ou=people,dc=example,dc=org +changetype: modify +add: givenName +givenName: Some +- +delete: mail +- +delete: telephoneNumber +telephoneNumber: 123 456 789 - 0 +- +replace: sn +sn: One +- + +` + mod := ldap.NewModifyRequest("uid=someone,ou=people,dc=example,dc=org") + mod.Replace("sn", []string{"One"}) + mod.Add("givenName", []string{"Some"}) + mod.Delete("mail", []string{}) + mod.Delete("telephoneNumber", []string{"123 456 789 - 0"}) + l := &ldif.LDIF{ + Entries: []*ldif.Entry{ + {Modify: mod}, + }, + } + res, err := ldif.Marshal(l) + if err != nil { + t.Errorf("Failed to marshal entry: %s", err) + } + if res != modLDIF { + t.Errorf("unexpected result: >>%s<<", res) + } +} + +func TestMarshalAdd(t *testing.T) { + addLDIF := `dn: uid=someone,ou=people,dc=example,dc=org +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +uid: someone +cn: Someone +mail: someone@example.org + +` + add := ldap.NewAddRequest("uid=someone,ou=people,dc=example,dc=org") + for _, a := range entries[1].Attributes { + add.Attribute(a.Name, a.Values) + } + l := &ldif.LDIF{ + Entries: []*ldif.Entry{ + {Add: add}, + }, + } + res, err := ldif.Marshal(l) + if err != nil { + t.Errorf("Failed to marshal entry: %s", err) + } + if res != addLDIF { + t.Errorf("unexpected result: >>%s<<", res) + } +} + +func TestMarshalDel(t *testing.T) { + delLDIF := `dn: uid=someone,ou=people,dc=example,dc=org +changetype: delete + +` + del := ldap.NewDelRequest("uid=someone,ou=people,dc=example,dc=org", nil) + l := &ldif.LDIF{ + Entries: []*ldif.Entry{ + {Del: del}, + }, + } + res, err := ldif.Marshal(l) + if err != nil { + t.Errorf("Failed to marshal entry: %s", err) + } + if res != delLDIF { + t.Errorf("unexpected result: >>%s<<", res) + } +} + +func TestDump(t *testing.T) { + delLDIF := `dn: uid=someone,ou=people,dc=example,dc=org +changetype: delete + +` + del := ldap.NewDelRequest("uid=someone,ou=people,dc=example,dc=org", nil) + buf := bytes.NewBuffer(nil) + err := ldif.Dump(buf, 0, del) + if err != nil { + t.Errorf("Failed to dump entry: %s", err) + } + res := buf.String() + if res != delLDIF { + t.Errorf("unexpected result: >>%s<<", res) + } +} + +/* +func TestMarshalModDN(t *testing.T) { + moddnLDIF := `dn: uid=someone,ou=people,dc=example,dc=org +changetype: moddn +deleteoldrdn: 1 +newrdn: uid=somebody +newsuperior: ou=people,dc=example,dc=org +- + +` + mod := ldap.NewModifyDNRequest("uid=someone,ou=people,dc=example,dc=org", "uid=somebody", true, "ou=people,dc=example,dc=org") + l := &ldif.LDIF{ + Entries: []*ldif.Entry{ + {ModifyDN: mod}, + }, + } + res, err := ldif.Marshal(l) + if err != nil { + t.Errorf("Failed to marshal: %s", err) + } + if res != moddnLDIF { + t.Errorf("unexprected result: >>%s<<", res) + } +} +*/