Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add basic ldif reader #49

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions ldif/apply.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe as a future TODO, can we use an io.Writer here, or maybe pass in the log rather than using the static global?

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)
}
/*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not check in commented code either, unless there is some good reason.

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
}
327 changes: 327 additions & 0 deletions ldif/ldif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
// 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"
"gopkg.in/ldap.v2"
"io"
"io/ioutil"
"net/url"
// "os"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

"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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"

}

// The LDIF struct is used for parsing an LDIF
type LDIF struct {
Entries []*Entry
Version int
changeType string
FoldWidth int
}

// 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

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
}
lines = append(lines, line)
entry, perr := l.parseEntry(lines)
if perr != nil {
return &ParseError{Line: curLine, Message: perr.Error()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curLine here will always reference the blank line at the end of the entry, not the line in the entry that actually caused the problem

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with 94b60a2

}
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.Version == 0 && strings.HasPrefix(lines[0], "version:") {
Copy link
Contributor

@liggitt liggitt Aug 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like it would be better to make the version parsing an optional thing done on the first line of the file, rather than part of parseEntry. as it is, the first entry could be missing version line, and the second entry have one, and this would parse it out as the ldif version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed now in 8232180

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:]
}

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
}
Loading