github.com/hellobchain/third_party@v0.0.0-20230331131523-deb0478a2e52/ldap.v2/dn.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  //
     5  // File contains DN parsing functionallity
     6  //
     7  // https://tools.ietf.org/html/rfc4514
     8  //
     9  //   distinguishedName = [ relativeDistinguishedName
    10  //         *( COMMA relativeDistinguishedName ) ]
    11  //     relativeDistinguishedName = attributeTypeAndValue
    12  //         *( PLUS attributeTypeAndValue )
    13  //     attributeTypeAndValue = attributeType EQUALS attributeValue
    14  //     attributeType = descr / numericoid
    15  //     attributeValue = string / hexstring
    16  //
    17  //     ; The following characters are to be escaped when they appear
    18  //     ; in the value to be encoded: ESC, one of <escaped>, leading
    19  //     ; SHARP or SPACE, trailing SPACE, and NULL.
    20  //     string =   [ ( leadchar / pair ) [ *( stringchar / pair )
    21  //        ( trailchar / pair ) ] ]
    22  //
    23  //     leadchar = LUTF1 / UTFMB
    24  //     LUTF1 = %x01-1F / %x21 / %x24-2A / %x2D-3A /
    25  //        %x3D / %x3F-5B / %x5D-7F
    26  //
    27  //     trailchar  = TUTF1 / UTFMB
    28  //     TUTF1 = %x01-1F / %x21 / %x23-2A / %x2D-3A /
    29  //        %x3D / %x3F-5B / %x5D-7F
    30  //
    31  //     stringchar = SUTF1 / UTFMB
    32  //     SUTF1 = %x01-21 / %x23-2A / %x2D-3A /
    33  //        %x3D / %x3F-5B / %x5D-7F
    34  //
    35  //     pair = ESC ( ESC / special / hexpair )
    36  //     special = escaped / SPACE / SHARP / EQUALS
    37  //     escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE
    38  //     hexstring = SHARP 1*hexpair
    39  //     hexpair = HEX HEX
    40  //
    41  //  where the productions <descr>, <numericoid>, <COMMA>, <DQUOTE>,
    42  //  <EQUALS>, <ESC>, <HEX>, <LANGLE>, <NULL>, <PLUS>, <RANGLE>, <SEMI>,
    43  //  <SPACE>, <SHARP>, and <UTFMB> are defined in [RFC4512].
    44  //
    45  
    46  package ldap
    47  
    48  import (
    49  	"bytes"
    50  	enchex "encoding/hex"
    51  	"errors"
    52  	"fmt"
    53  	"strings"
    54  
    55  	ber "gopkg.in/asn1-ber.v1"
    56  )
    57  
    58  // AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
    59  type AttributeTypeAndValue struct {
    60  	// Type is the attribute type
    61  	Type string
    62  	// Value is the attribute value
    63  	Value string
    64  }
    65  
    66  // RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
    67  type RelativeDN struct {
    68  	Attributes []*AttributeTypeAndValue
    69  }
    70  
    71  // DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
    72  type DN struct {
    73  	RDNs []*RelativeDN
    74  }
    75  
    76  // ParseDN returns a distinguishedName or an error
    77  func ParseDN(str string) (*DN, error) {
    78  	dn := new(DN)
    79  	dn.RDNs = make([]*RelativeDN, 0)
    80  	rdn := new(RelativeDN)
    81  	rdn.Attributes = make([]*AttributeTypeAndValue, 0)
    82  	buffer := bytes.Buffer{}
    83  	attribute := new(AttributeTypeAndValue)
    84  	escaping := false
    85  
    86  	unescapedTrailingSpaces := 0
    87  	stringFromBuffer := func() string {
    88  		s := buffer.String()
    89  		s = s[0 : len(s)-unescapedTrailingSpaces]
    90  		buffer.Reset()
    91  		unescapedTrailingSpaces = 0
    92  		return s
    93  	}
    94  
    95  	for i := 0; i < len(str); i++ {
    96  		char := str[i]
    97  		if escaping {
    98  			unescapedTrailingSpaces = 0
    99  			escaping = false
   100  			switch char {
   101  			case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\':
   102  				buffer.WriteByte(char)
   103  				continue
   104  			}
   105  			// Not a special character, assume hex encoded octet
   106  			if len(str) == i+1 {
   107  				return nil, errors.New("Got corrupted escaped character")
   108  			}
   109  
   110  			dst := []byte{0}
   111  			n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2]))
   112  			if err != nil {
   113  				return nil, fmt.Errorf("Failed to decode escaped character: %s", err)
   114  			} else if n != 1 {
   115  				return nil, fmt.Errorf("Expected 1 byte when un-escaping, got %d", n)
   116  			}
   117  			buffer.WriteByte(dst[0])
   118  			i++
   119  		} else if char == '\\' {
   120  			unescapedTrailingSpaces = 0
   121  			escaping = true
   122  		} else if char == '=' {
   123  			attribute.Type = stringFromBuffer()
   124  			// Special case: If the first character in the value is # the
   125  			// following data is BER encoded so we can just fast forward
   126  			// and decode.
   127  			if len(str) > i+1 && str[i+1] == '#' {
   128  				i += 2
   129  				index := strings.IndexAny(str[i:], ",+")
   130  				data := str
   131  				if index > 0 {
   132  					data = str[i : i+index]
   133  				} else {
   134  					data = str[i:]
   135  				}
   136  				rawBER, err := enchex.DecodeString(data)
   137  				if err != nil {
   138  					return nil, fmt.Errorf("Failed to decode BER encoding: %s", err)
   139  				}
   140  				packet := ber.DecodePacket(rawBER)
   141  				buffer.WriteString(packet.Data.String())
   142  				i += len(data) - 1
   143  			}
   144  		} else if char == ',' || char == '+' {
   145  			// We're done with this RDN or value, push it
   146  			attribute.Value = stringFromBuffer()
   147  			rdn.Attributes = append(rdn.Attributes, attribute)
   148  			attribute = new(AttributeTypeAndValue)
   149  			if char == ',' {
   150  				dn.RDNs = append(dn.RDNs, rdn)
   151  				rdn = new(RelativeDN)
   152  				rdn.Attributes = make([]*AttributeTypeAndValue, 0)
   153  			}
   154  		} else if char == ' ' && buffer.Len() == 0 {
   155  			// ignore unescaped leading spaces
   156  			continue
   157  		} else {
   158  			if char == ' ' {
   159  				// Track unescaped spaces in case they are trailing and we need to remove them
   160  				unescapedTrailingSpaces++
   161  			} else {
   162  				// Reset if we see a non-space char
   163  				unescapedTrailingSpaces = 0
   164  			}
   165  			buffer.WriteByte(char)
   166  		}
   167  	}
   168  	if buffer.Len() > 0 {
   169  		if len(attribute.Type) == 0 {
   170  			return nil, errors.New("DN ended with incomplete type, value pair")
   171  		}
   172  		attribute.Value = stringFromBuffer()
   173  		rdn.Attributes = append(rdn.Attributes, attribute)
   174  		dn.RDNs = append(dn.RDNs, rdn)
   175  	}
   176  	return dn, nil
   177  }
   178  
   179  // Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
   180  // Returns true if they have the same number of relative distinguished names
   181  // and corresponding relative distinguished names (by position) are the same.
   182  func (d *DN) Equal(other *DN) bool {
   183  	if len(d.RDNs) != len(other.RDNs) {
   184  		return false
   185  	}
   186  	for i := range d.RDNs {
   187  		if !d.RDNs[i].Equal(other.RDNs[i]) {
   188  			return false
   189  		}
   190  	}
   191  	return true
   192  }
   193  
   194  // AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN.
   195  // "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com"
   196  // "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com"
   197  // "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com"
   198  func (d *DN) AncestorOf(other *DN) bool {
   199  	if len(d.RDNs) >= len(other.RDNs) {
   200  		return false
   201  	}
   202  	// Take the last `len(d.RDNs)` RDNs from the other DN to compare against
   203  	otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):]
   204  	for i := range d.RDNs {
   205  		if !d.RDNs[i].Equal(otherRDNs[i]) {
   206  			return false
   207  		}
   208  	}
   209  	return true
   210  }
   211  
   212  // Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
   213  // Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues
   214  // and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type.
   215  // The order of attributes is not significant.
   216  // Case of attribute types is not significant.
   217  func (r *RelativeDN) Equal(other *RelativeDN) bool {
   218  	if len(r.Attributes) != len(other.Attributes) {
   219  		return false
   220  	}
   221  	return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes)
   222  }
   223  
   224  func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool {
   225  	for _, attr := range attrs {
   226  		found := false
   227  		for _, myattr := range r.Attributes {
   228  			if myattr.Equal(attr) {
   229  				found = true
   230  				break
   231  			}
   232  		}
   233  		if !found {
   234  			return false
   235  		}
   236  	}
   237  	return true
   238  }
   239  
   240  // Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue
   241  // Case of the attribute type is not significant
   242  func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool {
   243  	return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value
   244  }