-
Notifications
You must be signed in to change notification settings - Fork 363
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
Changes from 14 commits
626a90b
5b2dc4d
33174f5
47139c1
553040a
9b058ac
039842e
b28402d
f5b1f54
80a4fdf
88b0c4c
9aeb445
f76f90f
bdac54f
94b60a2
8232180
80c0aa1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
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) | ||
} | ||
/* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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?