From fbde2a7c049cbcc37c46d951f604dfca08417e4d Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Wed, 8 Feb 2017 19:04:19 -0800 Subject: [PATCH] Add String() methods to DN and its subtypes This patch adds `String() string` methods to each of the following types: - DN - RelativeDN - AttributeTypeAndValue So that a `*DN` implements the `fmt.Stringer` interface. These methods also produce normalized strings: Attribute Type and Value are lowercased and joined with a "=" character while multiple attributes of a Relative DN are sorted lexicographically before being joined witha "+" character. This allows one to use the string representation of a DN as a map key and ensure that two DNs which `Equal()` eachother would have the same `String()` value. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- dn.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ dn_test.go | 89 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 127 insertions(+), 42 deletions(-) diff --git a/dn.go b/dn.go index a8ece314..13cc6f64 100644 --- a/dn.go +++ b/dn.go @@ -50,6 +50,7 @@ import ( enchex "encoding/hex" "errors" "fmt" + "sort" "strings" ber "gopkg.in/asn1-ber.v1" @@ -63,16 +64,95 @@ type AttributeTypeAndValue struct { Value string } +// String returns a normalized string representation of this attribute type and +// value pair which is the a lowercased join of the Type and Value with a "=". +func (a *AttributeTypeAndValue) String() string { + return strings.ToLower(a.Type) + "=" + a.encodeValue() +} + +func (a *AttributeTypeAndValue) encodeValue() string { + // Normalize the value first. + // value := strings.ToLower(a.Value) + value := a.Value + + encodedBuf := bytes.Buffer{} + + escapeChar := func(c byte) { + encodedBuf.WriteByte('\\') + encodedBuf.WriteByte(c) + } + + escapeHex := func(c byte) { + encodedBuf.WriteByte('\\') + encodedBuf.WriteString(enchex.EncodeToString([]byte{c})) + } + + for i := 0; i < len(value); i++ { + char := value[i] + if i == 0 && char == ' ' || char == '#' { + // Special case leading space or number sign. + escapeChar(char) + continue + } + if i == len(value)-1 && char == ' ' { + // Special case trailing space. + escapeChar(char) + continue + } + + switch char { + case '"', '+', ',', ';', '<', '>', '\\': + // Each of these special characters must be escaped. + escapeChar(char) + continue + } + + if char < ' ' || char > '~' { + // All special character escapes are handled first + // above. All bytes less than ASCII SPACE and all bytes + // greater than ASCII TILDE must be hex-escaped. + escapeHex(char) + continue + } + + // Any other character does not require escaping. + encodedBuf.WriteByte(char) + } + + return encodedBuf.String() +} + // RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 type RelativeDN struct { Attributes []*AttributeTypeAndValue } +// String returns a normalized string representation of this relative DN which +// is the a join of all attributes (sorted in increasing order) with a "+". +func (r *RelativeDN) String() string { + attrs := make([]string, len(r.Attributes)) + for i := range r.Attributes { + attrs[i] = r.Attributes[i].String() + } + sort.Strings(attrs) + return strings.Join(attrs, "+") +} + // DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 type DN struct { RDNs []*RelativeDN } +// String returns a normalized string representation of this DN which is the +// join of all relative DNs with a ",". +func (d *DN) String() string { + rdns := make([]string, len(d.RDNs)) + for i := range d.RDNs { + rdns[i] = d.RDNs[i].String() + } + return strings.Join(rdns, ",") +} + // ParseDN returns a distinguishedName or an error func ParseDN(str string) (*DN, error) { dn := new(DN) diff --git a/dn_test.go b/dn_test.go index 5055cc15..3dde6535 100644 --- a/dn_test.go +++ b/dn_test.go @@ -9,44 +9,44 @@ import ( func TestSuccessfulDNParsing(t *testing.T) { testcases := map[string]ldap.DN{ - "": ldap.DN{[]*ldap.RelativeDN{}}, - "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "dummy"}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "com"}}}}}, - "UID=jsmith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"UID", "jsmith"}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, - "OU=Sales+CN=J. Smith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{ - &ldap.AttributeTypeAndValue{"OU", "Sales"}, - &ldap.AttributeTypeAndValue{"CN", "J. Smith"}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, - "1.3.6.1.4.1.1466.0=#04024869": ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}}}, - "1.3.6.1.4.1.1466.0=#04024869,DC=net": ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, - "CN=Lu\\C4\\8Di\\C4\\87": ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"CN", "Lučić"}}}}}, - " CN = Lu\\C4\\8Di\\C4\\87 ": ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"CN", "Lučić"}}}}}, - ` A = 1 , B = 2 `: ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"A", "1"}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"B", "2"}}}}}, - ` A = 1 + B = 2 `: ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{ - &ldap.AttributeTypeAndValue{"A", "1"}, - &ldap.AttributeTypeAndValue{"B", "2"}}}}}, - ` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{" A ", " 1 "}}}, - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{" B ", " 2 "}}}}}, - ` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: ldap.DN{[]*ldap.RelativeDN{ - &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{ - &ldap.AttributeTypeAndValue{" A ", " 1 "}, - &ldap.AttributeTypeAndValue{" B ", " 2 "}}}}}, + "": {[]*ldap.RelativeDN{}}, + "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{{"cn", "Jim, \"Hasse Hö\" Hansson!"}}}, + {[]*ldap.AttributeTypeAndValue{{"dc", "dummy"}}}, + {[]*ldap.AttributeTypeAndValue{{"dc", "com"}}}}}, + "UID=jsmith,DC=example,DC=net": {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{{"UID", "jsmith"}}}, + {[]*ldap.AttributeTypeAndValue{{"DC", "example"}}}, + {[]*ldap.AttributeTypeAndValue{{"DC", "net"}}}}}, + "OU=Sales+CN=J. Smith,DC=example,DC=net": {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{ + {"OU", "Sales"}, + {"CN", "J. Smith"}}}, + {[]*ldap.AttributeTypeAndValue{{"DC", "example"}}}, + {[]*ldap.AttributeTypeAndValue{{"DC", "net"}}}}}, + "1.3.6.1.4.1.1466.0=#04024869": {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}}}, + "1.3.6.1.4.1.1466.0=#04024869,DC=net": {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}, + {[]*ldap.AttributeTypeAndValue{{"DC", "net"}}}}}, + "CN=Lu\\C4\\8Di\\C4\\87": {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{{"CN", "Lučić"}}}}}, + " CN = Lu\\C4\\8Di\\C4\\87 ": {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{{"CN", "Lučić"}}}}}, + ` A = 1 , B = 2 `: {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{{"A", "1"}}}, + {[]*ldap.AttributeTypeAndValue{{"B", "2"}}}}}, + ` A = 1 + B = 2 `: {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{ + {"A", "1"}, + {"B", "2"}}}}}, + ` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{{" A ", " 1 "}}}, + {[]*ldap.AttributeTypeAndValue{{" B ", " 2 "}}}}}, + ` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: {[]*ldap.RelativeDN{ + {[]*ldap.AttributeTypeAndValue{ + {" A ", " 1 "}, + {" B ", " 2 "}}}}}, } for test, answer := range testcases { @@ -137,8 +137,8 @@ func TestDNEqual(t *testing.T) { }, // Difference in leading/trailing chars is ignored { - "cn=John Doe, ou=People, dc=sun.com", - "cn=John Doe,ou=People,dc=sun.com", + "cn=\\ John\\20Doe, ou=People, dc=sun.com", + "cn= \\ John Doe,ou=People,dc=sun.com", true, }, // Difference in values is significant @@ -161,11 +161,16 @@ func TestDNEqual(t *testing.T) { continue } if expected, actual := tc.Equal, a.Equal(b); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) continue } if expected, actual := tc.Equal, b.Equal(a); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) + continue + } + + if expected, actual := a.Equal(b), a.String() == b.String(); expected != actual { + t.Errorf("%d: when asserting string comparison of %q and %q expected equal %v, got %v", i, a, b, expected, actual) continue } }