package ldap import ( "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) // Attribute represents an LDAP attribute type Attribute struct { // Type is the name of the LDAP attribute Type string // Vals are the LDAP attribute values Vals []string } func (a *Attribute) encode() *ber.Packet { seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute") seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.Type, "Type")) set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") for _, value := range a.Vals { set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) } seq.AppendChild(set) return seq } // AddRequest represents an LDAP AddRequest operation type AddRequest struct { // DN identifies the entry being added DN string // Attributes list the attributes of the new entry Attributes []Attribute // Controls hold optional controls to send with the request Controls []Control } func (req *AddRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationAddRequest, nil, "Add Request") pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) attributes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") for _, attribute := range req.Attributes { attributes.AppendChild(attribute.encode()) } pkt.AppendChild(attributes) envelope.AppendChild(pkt) if len(req.Controls) > 0 { envelope.AppendChild(encodeControls(req.Controls)) } return nil } // Attribute adds an attribute with the given type and values func (req *AddRequest) Attribute(attrType string, attrVals []string) { req.Attributes = append(req.Attributes, Attribute{Type: attrType, Vals: attrVals}) } // NewAddRequest returns an AddRequest for the given DN, with no attributes func NewAddRequest(dn string, controls []Control) *AddRequest { return &AddRequest{ DN: dn, Controls: controls, } } // Add performs the given AddRequest func (l *Conn) Add(addRequest *AddRequest) error { msgCtx, err := l.doRequest(addRequest) if err != nil { return err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return err } if packet.Children[1].Tag == ApplicationAddResponse { err := GetLDAPError(packet) if err != nil { return err } } else { return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) } return nil }
package ldap import ( "bytes" "crypto/md5" enchex "encoding/hex" "errors" "fmt" "io/ioutil" "math/rand" "strings" "github.com/Azure/go-ntlmssp" ber "github.com/go-asn1-ber/asn1-ber" ) // SimpleBindRequest represents a username/password bind operation type SimpleBindRequest struct { // Username is the name of the Directory object that the client wishes to bind as Username string // Password is the credentials to bind with Password string // Controls are optional controls to send with the bind request Controls []Control // AllowEmptyPassword sets whether the client allows binding with an empty password // (normally used for unauthenticated bind). AllowEmptyPassword bool } // SimpleBindResult contains the response from the server type SimpleBindResult struct { Controls []Control } // NewSimpleBindRequest returns a bind request func NewSimpleBindRequest(username string, password string, controls []Control) *SimpleBindRequest { return &SimpleBindRequest{ Username: username, Password: password, Controls: controls, AllowEmptyPassword: false, } } func (req *SimpleBindRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Username, "User Name")) pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.Password, "Password")) envelope.AppendChild(pkt) if len(req.Controls) > 0 { envelope.AppendChild(encodeControls(req.Controls)) } return nil } // SimpleBind performs the simple bind operation defined in the given request func (l *Conn) SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error) { if simpleBindRequest.Password == "" && !simpleBindRequest.AllowEmptyPassword { return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) } msgCtx, err := l.doRequest(simpleBindRequest) if err != nil { return nil, err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return nil, err } result := &SimpleBindResult{ Controls: make([]Control, 0), } if len(packet.Children) == 3 { for _, child := range packet.Children[2].Children { decodedChild, decodeErr := DecodeControl(child) if decodeErr != nil { return nil, fmt.Errorf("failed to decode child control: %s", decodeErr) } result.Controls = append(result.Controls, decodedChild) } } err = GetLDAPError(packet) return result, err } // Bind performs a bind with the given username and password. // // It does not allow unauthenticated bind (i.e. empty password). Use the UnauthenticatedBind method // for that. func (l *Conn) Bind(username, password string) error { req := &SimpleBindRequest{ Username: username, Password: password, AllowEmptyPassword: false, } _, err := l.SimpleBind(req) return err } // UnauthenticatedBind performs an unauthenticated bind. // // A username may be provided for trace (e.g. logging) purpose only, but it is normally not // authenticated or otherwise validated by the LDAP server. // // See https://tools.ietf.org/html/rfc4513#section-5.1.2 . // See https://tools.ietf.org/html/rfc4513#section-6.3.1 . func (l *Conn) UnauthenticatedBind(username string) error { req := &SimpleBindRequest{ Username: username, Password: "", AllowEmptyPassword: true, } _, err := l.SimpleBind(req) return err } // DigestMD5BindRequest represents a digest-md5 bind operation type DigestMD5BindRequest struct { Host string // Username is the name of the Directory object that the client wishes to bind as Username string // Password is the credentials to bind with Password string // Controls are optional controls to send with the bind request Controls []Control } func (req *DigestMD5BindRequest) appendTo(envelope *ber.Packet) error { request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "DIGEST-MD5", "SASL Mech")) request.AppendChild(auth) envelope.AppendChild(request) if len(req.Controls) > 0 { envelope.AppendChild(encodeControls(req.Controls)) } return nil } // DigestMD5BindResult contains the response from the server type DigestMD5BindResult struct { Controls []Control } // MD5Bind performs a digest-md5 bind with the given host, username and password. func (l *Conn) MD5Bind(host, username, password string) error { req := &DigestMD5BindRequest{ Host: host, Username: username, Password: password, } _, err := l.DigestMD5Bind(req) return err } // DigestMD5Bind performs the digest-md5 bind operation defined in the given request func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*DigestMD5BindResult, error) { if digestMD5BindRequest.Password == "" { return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) } msgCtx, err := l.doRequest(digestMD5BindRequest) if err != nil { return nil, err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return nil, err } l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if l.Debug { if err = addLDAPDescriptions(packet); err != nil { return nil, err } ber.PrintPacket(packet) } result := &DigestMD5BindResult{ Controls: make([]Control, 0), } var params map[string]string if len(packet.Children) == 2 { if len(packet.Children[1].Children) == 4 { child := packet.Children[1].Children[0] if child.Tag != ber.TagEnumerated { return result, GetLDAPError(packet) } if child.Value.(int64) != 14 { return result, GetLDAPError(packet) } child = packet.Children[1].Children[3] if child.Tag != ber.TagObjectDescriptor { return result, GetLDAPError(packet) } if child.Data == nil { return result, GetLDAPError(packet) } data, _ := ioutil.ReadAll(child.Data) params, err = parseParams(string(data)) if err != nil { return result, fmt.Errorf("parsing digest-challenge: %s", err) } } } if params != nil { resp := computeResponse( params, "ldap/"+strings.ToLower(digestMD5BindRequest.Host), digestMD5BindRequest.Username, digestMD5BindRequest.Password, ) packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "DIGEST-MD5", "SASL Mech")) auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, resp, "Credentials")) request.AppendChild(auth) packet.AppendChild(request) msgCtx, err = l.sendMessage(packet) if err != nil { return nil, fmt.Errorf("send message: %s", err) } defer l.finishMessage(msgCtx) packetResponse, ok := <-msgCtx.responses if !ok { return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) } packet, err = packetResponse.ReadPacket() l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if err != nil { return nil, fmt.Errorf("read packet: %s", err) } } err = GetLDAPError(packet) return result, err } func parseParams(str string) (map[string]string, error) { m := make(map[string]string) var key, value string var state int for i := 0; i <= len(str); i++ { switch state { case 0: // reading key if i == len(str) { return nil, fmt.Errorf("syntax error on %d", i) } if str[i] != '=' { key += string(str[i]) continue } state = 1 case 1: // reading value if i == len(str) { m[key] = value break } switch str[i] { case ',': m[key] = value state = 0 key = "" value = "" case '"': if value != "" { return nil, fmt.Errorf("syntax error on %d", i) } state = 2 default: value += string(str[i]) } case 2: // inside quotes if i == len(str) { return nil, fmt.Errorf("syntax error on %d", i) } if str[i] != '"' { value += string(str[i]) } else { state = 1 } } } return m, nil } func computeResponse(params map[string]string, uri, username, password string) string { nc := "00000001" qop := "auth" cnonce := enchex.EncodeToString(randomBytes(16)) x := username + ":" + params["realm"] + ":" + password y := md5Hash([]byte(x)) a1 := bytes.NewBuffer(y) a1.WriteString(":" + params["nonce"] + ":" + cnonce) if len(params["authzid"]) > 0 { a1.WriteString(":" + params["authzid"]) } a2 := bytes.NewBuffer([]byte("AUTHENTICATE")) a2.WriteString(":" + uri) ha1 := enchex.EncodeToString(md5Hash(a1.Bytes())) ha2 := enchex.EncodeToString(md5Hash(a2.Bytes())) kd := ha1 kd += ":" + params["nonce"] kd += ":" + nc kd += ":" + cnonce kd += ":" + qop kd += ":" + ha2 resp := enchex.EncodeToString(md5Hash([]byte(kd))) return fmt.Sprintf( `username="%s",realm="%s",nonce="%s",cnonce="%s",nc=00000001,qop=%s,digest-uri="%s",response=%s`, username, params["realm"], params["nonce"], cnonce, qop, uri, resp, ) } func md5Hash(b []byte) []byte { hasher := md5.New() hasher.Write(b) return hasher.Sum(nil) } func randomBytes(len int) []byte { b := make([]byte, len) for i := 0; i < len; i++ { b[i] = byte(rand.Intn(256)) } return b } var externalBindRequest = requestFunc(func(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) saslAuth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") saslAuth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "EXTERNAL", "SASL Mech")) saslAuth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "SASL Cred")) pkt.AppendChild(saslAuth) envelope.AppendChild(pkt) return nil }) // ExternalBind performs SASL/EXTERNAL authentication. // // Use ldap.DialURL("ldapi://") to connect to the Unix socket before ExternalBind. // // See https://tools.ietf.org/html/rfc4422#appendix-A func (l *Conn) ExternalBind() error { msgCtx, err := l.doRequest(externalBindRequest) if err != nil { return err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return err } return GetLDAPError(packet) } // NTLMBind performs an NTLMSSP bind leveraging https://github.com/Azure/go-ntlmssp // NTLMBindRequest represents an NTLMSSP bind operation type NTLMBindRequest struct { // Domain is the AD Domain to authenticate too. If not specified, it will be grabbed from the NTLMSSP Challenge Domain string // Username is the name of the Directory object that the client wishes to bind as Username string // Password is the credentials to bind with Password string // AllowEmptyPassword sets whether the client allows binding with an empty password // (normally used for unauthenticated bind). AllowEmptyPassword bool // Hash is the hex NTLM hash to bind with. Password or hash must be provided Hash string // Controls are optional controls to send with the bind request Controls []Control } func (req *NTLMBindRequest) appendTo(envelope *ber.Packet) error { request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) // generate an NTLMSSP Negotiation message for the specified domain (it can be blank) negMessage, err := ntlmssp.NewNegotiateMessage(req.Domain, "") if err != nil { return fmt.Errorf("err creating negmessage: %s", err) } // append the generated NTLMSSP message as a TagEnumerated BER value auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEnumerated, negMessage, "authentication") request.AppendChild(auth) envelope.AppendChild(request) if len(req.Controls) > 0 { envelope.AppendChild(encodeControls(req.Controls)) } return nil } // NTLMBindResult contains the response from the server type NTLMBindResult struct { Controls []Control } // NTLMBind performs an NTLMSSP Bind with the given domain, username and password func (l *Conn) NTLMBind(domain, username, password string) error { req := &NTLMBindRequest{ Domain: domain, Username: username, Password: password, } _, err := l.NTLMChallengeBind(req) return err } // NTLMUnauthenticatedBind performs an bind with an empty password. // // A username is required. The anonymous bind is not (yet) supported by the go-ntlmssp library (https://github.com/Azure/go-ntlmssp/blob/819c794454d067543bc61d29f61fef4b3c3df62c/authenticate_message.go#L87) // // See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 part 3.2.5.1.2 func (l *Conn) NTLMUnauthenticatedBind(domain, username string) error { req := &NTLMBindRequest{ Domain: domain, Username: username, Password: "", AllowEmptyPassword: true, } _, err := l.NTLMChallengeBind(req) return err } // NTLMBindWithHash performs an NTLM Bind with an NTLM hash instead of plaintext password (pass-the-hash) func (l *Conn) NTLMBindWithHash(domain, username, hash string) error { req := &NTLMBindRequest{ Domain: domain, Username: username, Hash: hash, } _, err := l.NTLMChallengeBind(req) return err } // NTLMChallengeBind performs the NTLMSSP bind operation defined in the given request func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindResult, error) { if !ntlmBindRequest.AllowEmptyPassword && ntlmBindRequest.Password == "" && ntlmBindRequest.Hash == "" { return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) } msgCtx, err := l.doRequest(ntlmBindRequest) if err != nil { return nil, err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return nil, err } l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if l.Debug { if err = addLDAPDescriptions(packet); err != nil { return nil, err } ber.PrintPacket(packet) } result := &NTLMBindResult{ Controls: make([]Control, 0), } var ntlmsspChallenge []byte // now find the NTLM Response Message if len(packet.Children) == 2 { if len(packet.Children[1].Children) == 3 { child := packet.Children[1].Children[1] ntlmsspChallenge = child.ByteValue // Check to make sure we got the right message. It will always start with NTLMSSP if len(ntlmsspChallenge) < 7 || !bytes.Equal(ntlmsspChallenge[:7], []byte("NTLMSSP")) { return result, GetLDAPError(packet) } l.Debug.Printf("%d: found ntlmssp challenge", msgCtx.id) } } if ntlmsspChallenge != nil { var err error var responseMessage []byte // generate a response message to the challenge with the given Username/Password if password is provided if ntlmBindRequest.Hash != "" { responseMessage, err = ntlmssp.ProcessChallengeWithHash(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Hash) } else if ntlmBindRequest.Password != "" || ntlmBindRequest.AllowEmptyPassword { _, _, domainNeeded := ntlmssp.GetDomain(ntlmBindRequest.Username) responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password, domainNeeded) } else { err = fmt.Errorf("need a password or hash to generate reply") } if err != nil { return result, fmt.Errorf("parsing ntlm-challenge: %s", err) } packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) // append the challenge response message as a TagEmbeddedPDV BER value auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEmbeddedPDV, responseMessage, "authentication") request.AppendChild(auth) packet.AppendChild(request) msgCtx, err = l.sendMessage(packet) if err != nil { return nil, fmt.Errorf("send message: %s", err) } defer l.finishMessage(msgCtx) packetResponse, ok := <-msgCtx.responses if !ok { return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) } packet, err = packetResponse.ReadPacket() l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if err != nil { return nil, fmt.Errorf("read packet: %s", err) } } err = GetLDAPError(packet) return result, err } // GSSAPIClient interface is used as the client-side implementation for the // GSSAPI SASL mechanism. // Interface inspired by GSSAPIClient from golang.org/x/crypto/ssh type GSSAPIClient interface { // InitSecContext initiates the establishment of a security context for // GSS-API between the client and server. // Initially the token parameter should be specified as nil. // The routine may return a outputToken which should be transferred to // the server, where the server will present it to AcceptSecContext. // If no token need be sent, InitSecContext will indicate this by setting // needContinue to false. To complete the context // establishment, one or more reply tokens may be required from the server; // if so, InitSecContext will return a needContinue which is true. // In this case, InitSecContext should be called again when the // reply token is received from the server, passing the reply token // to InitSecContext via the token parameters. // See RFC 4752 section 3.1. InitSecContext(target string, token []byte) (outputToken []byte, needContinue bool, err error) // NegotiateSaslAuth performs the last step of the Sasl handshake. // It takes a token, which, when unwrapped, describes the servers supported // security layers (first octet) and maximum receive buffer (remaining // three octets). // If the received token is unacceptable an error must be returned to abort // the handshake. // Outputs a signed token describing the client's selected security layer // and receive buffer size and optionally an authorization identity. // The returned token will be sent to the server and the handshake considered // completed successfully and the server authenticated. // See RFC 4752 section 3.1. NegotiateSaslAuth(token []byte, authzid string) ([]byte, error) // DeleteSecContext destroys any established secure context. DeleteSecContext() error } // GSSAPIBindRequest represents a GSSAPI SASL mechanism bind request. // See rfc4752 and rfc4513 section 5.2.1.2. type GSSAPIBindRequest struct { // Service Principal Name user for the service ticket. Eg. "ldap/<host>" ServicePrincipalName string // (Optional) Authorization entity AuthZID string // (Optional) Controls to send with the bind request Controls []Control } // GSSAPIBind performs the GSSAPI SASL bind using the provided GSSAPI client. func (l *Conn) GSSAPIBind(client GSSAPIClient, servicePrincipal, authzid string) error { return l.GSSAPIBindRequest(client, &GSSAPIBindRequest{ ServicePrincipalName: servicePrincipal, AuthZID: authzid, }) } // GSSAPIBindRequest performs the GSSAPI SASL bind using the provided GSSAPI client. func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error { //nolint:errcheck defer client.DeleteSecContext() var err error var reqToken []byte var recvToken []byte needInit := true for { if needInit { // Establish secure context between client and server. reqToken, needInit, err = client.InitSecContext(req.ServicePrincipalName, recvToken) if err != nil { return err } } else { // Secure context is set up, perform the last step of SASL handshake. reqToken, err = client.NegotiateSaslAuth(recvToken, req.AuthZID) if err != nil { return err } } // Send Bind request containing the current token and extract the // token sent by server. recvToken, err = l.saslBindTokenExchange(req.Controls, reqToken) if err != nil { return err } if !needInit && len(recvToken) == 0 { break } } return nil } func (l *Conn) saslBindTokenExchange(reqControls []Control, reqToken []byte) ([]byte, error) { // Construct LDAP Bind request with GSSAPI SASL mechanism. envelope := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") envelope.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "GSSAPI", "SASL Mech")) if len(reqToken) > 0 { auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(reqToken), "Credentials")) } request.AppendChild(auth) envelope.AppendChild(request) if len(reqControls) > 0 { envelope.AppendChild(encodeControls(reqControls)) } msgCtx, err := l.sendMessage(envelope) if err != nil { return nil, err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return nil, err } l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if l.Debug { if err = addLDAPDescriptions(packet); err != nil { return nil, err } ber.PrintPacket(packet) } // https://www.rfc-editor.org/rfc/rfc4511#section-4.1.1 // packet is an envelope // child 0 is message id // child 1 is protocolOp if len(packet.Children) != 2 { return nil, fmt.Errorf("bad bind response") } protocolOp := packet.Children[1] RESP: switch protocolOp.Description { case "Bind Response": // Bind Response // Bind Reponse is an LDAP Response (https://www.rfc-editor.org/rfc/rfc4511#section-4.1.9) // with an additional optional serverSaslCreds string (https://www.rfc-editor.org/rfc/rfc4511#section-4.2.2) // child 0 is resultCode resultCode := protocolOp.Children[0] if resultCode.Tag != ber.TagEnumerated { break RESP } switch resultCode.Value.(int64) { case 14: // Sasl bind in progress if len(protocolOp.Children) < 3 { break RESP } referral := protocolOp.Children[3] switch referral.Description { case "Referral": if referral.ClassType != ber.ClassContext || referral.Tag != ber.TagObjectDescriptor { break RESP } return ioutil.ReadAll(referral.Data) } // Optional: //if len(protocolOp.Children) == 4 { // serverSaslCreds := protocolOp.Children[4] //} case 0: // Success - Bind OK. // SASL layer in effect (if any) (See https://www.rfc-editor.org/rfc/rfc4513#section-5.2.1.4) // NOTE: SASL security layers are not supported currently. return nil, nil } } return nil, GetLDAPError(packet) }
package ldap import ( "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) // CompareRequest represents an LDAP CompareRequest operation. type CompareRequest struct { DN string Attribute string Value string } func (req *CompareRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationCompareRequest, nil, "Compare Request") pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) ava := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "AttributeValueAssertion") ava.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Attribute, "AttributeDesc")) ava.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Value, "AssertionValue")) pkt.AppendChild(ava) envelope.AppendChild(pkt) return nil } // Compare checks to see if the attribute of the dn matches value. Returns true if it does otherwise // false with any error that occurs if any. func (l *Conn) Compare(dn, attribute, value string) (bool, error) { msgCtx, err := l.doRequest(&CompareRequest{ DN: dn, Attribute: attribute, Value: value, }) if err != nil { return false, err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return false, err } if packet.Children[1].Tag == ApplicationCompareResponse { err := GetLDAPError(packet) switch { case IsErrorWithCode(err, LDAPResultCompareTrue): return true, nil case IsErrorWithCode(err, LDAPResultCompareFalse): return false, nil default: return false, err } } return false, fmt.Errorf("unexpected Response: %d", packet.Children[1].Tag) }
package ldap import ( "bufio" "context" "crypto/tls" "errors" "fmt" "net" "net/url" "sync" "sync/atomic" "time" ber "github.com/go-asn1-ber/asn1-ber" ) const ( // MessageQuit causes the processMessages loop to exit MessageQuit = 0 // MessageRequest sends a request to the server MessageRequest = 1 // MessageResponse receives a response from the server MessageResponse = 2 // MessageFinish indicates the client considers a particular message ID to be finished MessageFinish = 3 // MessageTimeout indicates the client-specified timeout for a particular message ID has been reached MessageTimeout = 4 ) const ( // DefaultLdapPort default ldap port for pure TCP connection DefaultLdapPort = "389" // DefaultLdapsPort default ldap port for SSL connection DefaultLdapsPort = "636" ) // PacketResponse contains the packet or error encountered reading a response type PacketResponse struct { // Packet is the packet read from the server Packet *ber.Packet // Error is an error encountered while reading Error error } // ReadPacket returns the packet or an error func (pr *PacketResponse) ReadPacket() (*ber.Packet, error) { if (pr == nil) || (pr.Packet == nil && pr.Error == nil) { return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve response")) } return pr.Packet, pr.Error } type messageContext struct { id int64 // close(done) should only be called from finishMessage() done chan struct{} // close(responses) should only be called from processMessages(), and only sent to from sendResponse() responses chan *PacketResponse } // sendResponse should only be called within the processMessages() loop which // is also responsible for closing the responses channel. func (msgCtx *messageContext) sendResponse(packet *PacketResponse, timeout time.Duration) { timeoutCtx := context.Background() if timeout > 0 { var cancelFunc context.CancelFunc timeoutCtx, cancelFunc = context.WithTimeout(context.Background(), timeout) defer cancelFunc() } select { case msgCtx.responses <- packet: // Successfully sent packet to message handler. case <-msgCtx.done: // The request handler is done and will not receive more // packets. case <-timeoutCtx.Done(): // The timeout was reached before the packet was sent. } } type messagePacket struct { Op int MessageID int64 Packet *ber.Packet Context *messageContext } type sendMessageFlags uint const ( startTLS sendMessageFlags = 1 << iota ) // Conn represents an LDAP Connection type Conn struct { // requestTimeout is loaded atomically // so we need to ensure 64-bit alignment on 32-bit platforms. // https://github.com/go-ldap/ldap/pull/199 requestTimeout int64 conn net.Conn isTLS bool closing uint32 closeErr atomic.Value isStartingTLS bool Debug debugging chanConfirm chan struct{} messageContexts map[int64]*messageContext chanMessage chan *messagePacket chanMessageID chan int64 wgClose sync.WaitGroup outstandingRequests uint messageMutex sync.Mutex err error } var _ Client = &Conn{} // DefaultTimeout is a package-level variable that sets the timeout value // used for the Dial and DialTLS methods. // // WARNING: since this is a package-level variable, setting this value from // multiple places will probably result in undesired behaviour. var DefaultTimeout = 60 * time.Second // DialOpt configures DialContext. type DialOpt func(*DialContext) // DialWithDialer updates net.Dialer in DialContext. func DialWithDialer(d *net.Dialer) DialOpt { return func(dc *DialContext) { dc.dialer = d } } // DialWithTLSConfig updates tls.Config in DialContext. func DialWithTLSConfig(tc *tls.Config) DialOpt { return func(dc *DialContext) { dc.tlsConfig = tc } } // DialWithTLSDialer is a wrapper for DialWithTLSConfig with the option to // specify a net.Dialer to for example define a timeout or a custom resolver. // // Deprecated: Use DialWithDialer and DialWithTLSConfig instead func DialWithTLSDialer(tlsConfig *tls.Config, dialer *net.Dialer) DialOpt { return func(dc *DialContext) { dc.tlsConfig = tlsConfig dc.dialer = dialer } } // DialContext contains necessary parameters to dial the given ldap URL. type DialContext struct { dialer *net.Dialer tlsConfig *tls.Config } func (dc *DialContext) dial(u *url.URL) (net.Conn, error) { if u.Scheme == "ldapi" { if u.Path == "" || u.Path == "/" { u.Path = "/var/run/slapd/ldapi" } return dc.dialer.Dial("unix", u.Path) } host, port, err := net.SplitHostPort(u.Host) if err != nil { // we assume that error is due to missing port host = u.Host port = "" } switch u.Scheme { case "cldap": if port == "" { port = DefaultLdapPort } return dc.dialer.Dial("udp", net.JoinHostPort(host, port)) case "ldap": if port == "" { port = DefaultLdapPort } return dc.dialer.Dial("tcp", net.JoinHostPort(host, port)) case "ldaps": if port == "" { port = DefaultLdapsPort } return tls.DialWithDialer(dc.dialer, "tcp", net.JoinHostPort(host, port), dc.tlsConfig) } return nil, fmt.Errorf("Unknown scheme '%s'", u.Scheme) } // Dial connects to the given address on the given network using net.Dial // and then returns a new Conn for the connection. // // Deprecated: Use DialURL instead. func Dial(network, addr string) (*Conn, error) { c, err := net.DialTimeout(network, addr, DefaultTimeout) if err != nil { return nil, NewError(ErrorNetwork, err) } conn := NewConn(c, false) conn.Start() return conn, nil } // DialTLS connects to the given address on the given network using tls.Dial // and then returns a new Conn for the connection. // // Deprecated: Use DialURL instead. func DialTLS(network, addr string, config *tls.Config) (*Conn, error) { c, err := tls.DialWithDialer(&net.Dialer{Timeout: DefaultTimeout}, network, addr, config) if err != nil { return nil, NewError(ErrorNetwork, err) } conn := NewConn(c, true) conn.Start() return conn, nil } // DialURL connects to the given ldap URL. // The following schemas are supported: ldap://, ldaps://, ldapi://, // and cldap:// (RFC1798, deprecated but used by Active Directory). // On success a new Conn for the connection is returned. func DialURL(addr string, opts ...DialOpt) (*Conn, error) { u, err := url.Parse(addr) if err != nil { return nil, NewError(ErrorNetwork, err) } var dc DialContext for _, opt := range opts { opt(&dc) } if dc.dialer == nil { dc.dialer = &net.Dialer{Timeout: DefaultTimeout} } c, err := dc.dial(u) if err != nil { return nil, NewError(ErrorNetwork, err) } conn := NewConn(c, u.Scheme == "ldaps") conn.Start() return conn, nil } // NewConn returns a new Conn using conn for network I/O. func NewConn(conn net.Conn, isTLS bool) *Conn { l := &Conn{ conn: conn, chanConfirm: make(chan struct{}), chanMessageID: make(chan int64), chanMessage: make(chan *messagePacket, 10), messageContexts: map[int64]*messageContext{}, requestTimeout: 0, isTLS: isTLS, } l.wgClose.Add(1) return l } // Start initialises goroutines to read replies and process messages. // Warning: Calling this function in addition to Dial or DialURL // may cause race conditions. // // See: https://github.com/go-ldap/ldap/issues/356 func (l *Conn) Start() { go l.reader() go l.processMessages() } // IsClosing returns whether or not we're currently closing. func (l *Conn) IsClosing() bool { return atomic.LoadUint32(&l.closing) == 1 } // setClosing sets the closing value to true func (l *Conn) setClosing() bool { return atomic.CompareAndSwapUint32(&l.closing, 0, 1) } // Close closes the connection. func (l *Conn) Close() (err error) { l.messageMutex.Lock() defer l.messageMutex.Unlock() if l.setClosing() { l.Debug.Printf("Sending quit message and waiting for confirmation") l.chanMessage <- &messagePacket{Op: MessageQuit} timeoutCtx := context.Background() if l.getTimeout() > 0 { var cancelFunc context.CancelFunc timeoutCtx, cancelFunc = context.WithTimeout(timeoutCtx, time.Duration(l.getTimeout())) defer cancelFunc() } select { case <-l.chanConfirm: // Confirmation was received. case <-timeoutCtx.Done(): // The timeout was reached before confirmation was received. } close(l.chanMessage) l.Debug.Printf("Closing network connection") err = l.conn.Close() l.wgClose.Done() } l.wgClose.Wait() return err } // SetTimeout sets the time after a request is sent that a MessageTimeout triggers func (l *Conn) SetTimeout(timeout time.Duration) { atomic.StoreInt64(&l.requestTimeout, int64(timeout)) } func (l *Conn) getTimeout() int64 { return atomic.LoadInt64(&l.requestTimeout) } // Returns the next available messageID func (l *Conn) nextMessageID() int64 { if messageID, ok := <-l.chanMessageID; ok { return messageID } return 0 } // GetLastError returns the last recorded error from goroutines like processMessages and reader. // Only the last recorded error will be returned. func (l *Conn) GetLastError() error { l.messageMutex.Lock() defer l.messageMutex.Unlock() return l.err } // StartTLS sends the command to start a TLS session and then creates a new TLS Client func (l *Conn) StartTLS(config *tls.Config) error { if l.isTLS { return NewError(ErrorNetwork, errors.New("ldap: already encrypted")) } packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS") request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command")) packet.AppendChild(request) l.Debug.PrintPacket(packet) msgCtx, err := l.sendMessageWithFlags(packet, startTLS) if err != nil { return err } defer l.finishMessage(msgCtx) l.Debug.Printf("%d: waiting for response", msgCtx.id) packetResponse, ok := <-msgCtx.responses if !ok { return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) } packet, err = packetResponse.ReadPacket() l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if err != nil { return err } if l.Debug { if err := addLDAPDescriptions(packet); err != nil { l.Close() return err } l.Debug.PrintPacket(packet) } if err := GetLDAPError(packet); err == nil { conn := tls.Client(l.conn, config) if connErr := conn.Handshake(); connErr != nil { l.Close() return NewError(ErrorNetwork, fmt.Errorf("TLS handshake failed (%v)", connErr)) } l.isTLS = true l.conn = conn } else { return err } go l.reader() return nil } // TLSConnectionState returns the client's TLS connection state. // The return values are their zero values if StartTLS did // not succeed. func (l *Conn) TLSConnectionState() (state tls.ConnectionState, ok bool) { tc, ok := l.conn.(*tls.Conn) if !ok { return } return tc.ConnectionState(), true } func (l *Conn) sendMessage(packet *ber.Packet) (*messageContext, error) { return l.sendMessageWithFlags(packet, 0) } func (l *Conn) sendMessageWithFlags(packet *ber.Packet, flags sendMessageFlags) (*messageContext, error) { if l.IsClosing() { return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) } l.messageMutex.Lock() l.Debug.Printf("flags&startTLS = %d", flags&startTLS) if l.isStartingTLS { l.messageMutex.Unlock() return nil, NewError(ErrorNetwork, errors.New("ldap: connection is in startls phase")) } if flags&startTLS != 0 { if l.outstandingRequests != 0 { l.messageMutex.Unlock() return nil, NewError(ErrorNetwork, errors.New("ldap: cannot StartTLS with outstanding requests")) } l.isStartingTLS = true } l.outstandingRequests++ l.messageMutex.Unlock() responses := make(chan *PacketResponse) messageID := packet.Children[0].Value.(int64) message := &messagePacket{ Op: MessageRequest, MessageID: messageID, Packet: packet, Context: &messageContext{ id: messageID, done: make(chan struct{}), responses: responses, }, } if !l.sendProcessMessage(message) { if l.IsClosing() { return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) } return nil, NewError(ErrorNetwork, errors.New("ldap: could not send message for unknown reason")) } return message.Context, nil } func (l *Conn) finishMessage(msgCtx *messageContext) { close(msgCtx.done) if l.IsClosing() { return } l.messageMutex.Lock() l.outstandingRequests-- if l.isStartingTLS { l.isStartingTLS = false } l.messageMutex.Unlock() message := &messagePacket{ Op: MessageFinish, MessageID: msgCtx.id, } l.sendProcessMessage(message) } func (l *Conn) sendProcessMessage(message *messagePacket) bool { l.messageMutex.Lock() defer l.messageMutex.Unlock() if l.IsClosing() { return false } l.chanMessage <- message return true } func (l *Conn) processMessages() { defer func() { if err := recover(); err != nil { l.err = fmt.Errorf("ldap: recovered panic in processMessages: %v", err) } for messageID, msgCtx := range l.messageContexts { // If we are closing due to an error, inform anyone who // is waiting about the error. if l.IsClosing() && l.closeErr.Load() != nil { msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)}, time.Duration(l.getTimeout())) } l.Debug.Printf("Closing channel for MessageID %d", messageID) close(msgCtx.responses) delete(l.messageContexts, messageID) } close(l.chanMessageID) close(l.chanConfirm) }() var messageID int64 = 1 for { select { case l.chanMessageID <- messageID: messageID++ case message := <-l.chanMessage: switch message.Op { case MessageQuit: l.Debug.Printf("Shutting down - quit message received") return case MessageRequest: // Add to message list and write to network l.Debug.Printf("Sending message %d", message.MessageID) buf := message.Packet.Bytes() _, err := l.conn.Write(buf) if err != nil { l.Debug.Printf("Error Sending Message: %s", err.Error()) message.Context.sendResponse(&PacketResponse{Error: fmt.Errorf("unable to send request: %s", err)}, time.Duration(l.getTimeout())) close(message.Context.responses) break } // Only add to messageContexts if we were able to // successfully write the message. l.messageContexts[message.MessageID] = message.Context // Add timeout if defined requestTimeout := l.getTimeout() if requestTimeout > 0 { go func() { timer := time.NewTimer(time.Duration(requestTimeout)) defer func() { if err := recover(); err != nil { l.err = fmt.Errorf("ldap: recovered panic in RequestTimeout: %v", err) } timer.Stop() }() select { case <-timer.C: timeoutMessage := &messagePacket{ Op: MessageTimeout, MessageID: message.MessageID, } l.sendProcessMessage(timeoutMessage) case <-message.Context.done: } }() } case MessageResponse: l.Debug.Printf("Receiving message %d", message.MessageID) if msgCtx, ok := l.messageContexts[message.MessageID]; ok { msgCtx.sendResponse(&PacketResponse{message.Packet, nil}, time.Duration(l.getTimeout())) } else { l.err = fmt.Errorf("ldap: received unexpected message %d, %v", message.MessageID, l.IsClosing()) l.Debug.PrintPacket(message.Packet) } case MessageTimeout: // Handle the timeout by closing the channel // All reads will return immediately if msgCtx, ok := l.messageContexts[message.MessageID]; ok { l.Debug.Printf("Receiving message timeout for %d", message.MessageID) msgCtx.sendResponse(&PacketResponse{message.Packet, NewError(ErrorNetwork, errors.New("ldap: connection timed out"))}, time.Duration(l.getTimeout())) delete(l.messageContexts, message.MessageID) close(msgCtx.responses) } case MessageFinish: l.Debug.Printf("Finished message %d", message.MessageID) if msgCtx, ok := l.messageContexts[message.MessageID]; ok { delete(l.messageContexts, message.MessageID) close(msgCtx.responses) } } } } } func (l *Conn) reader() { cleanstop := false defer func() { if err := recover(); err != nil { l.err = fmt.Errorf("ldap: recovered panic in reader: %v", err) } if !cleanstop { l.Close() } }() bufConn := bufio.NewReader(l.conn) for { if cleanstop { l.Debug.Printf("reader clean stopping (without closing the connection)") return } packet, err := ber.ReadPacket(bufConn) if err != nil { // A read error is expected here if we are closing the connection... if !l.IsClosing() { l.closeErr.Store(fmt.Errorf("unable to read LDAP response packet: %s", err)) l.Debug.Printf("reader error: %s", err) } return } if err := addLDAPDescriptions(packet); err != nil { l.Debug.Printf("descriptions error: %s", err) } if len(packet.Children) == 0 { l.Debug.Printf("Received bad ldap packet") continue } l.messageMutex.Lock() if l.isStartingTLS { cleanstop = true } l.messageMutex.Unlock() message := &messagePacket{ Op: MessageResponse, MessageID: packet.Children[0].Value.(int64), Packet: packet, } if !l.sendProcessMessage(message) { return } } }
package ldap import ( "fmt" "strconv" ber "github.com/go-asn1-ber/asn1-ber" "github.com/google/uuid" ) const ( // ControlTypePaging - https://www.ietf.org/rfc/rfc2696.txt ControlTypePaging = "1.2.840.113556.1.4.319" // ControlTypeBeheraPasswordPolicy - https://tools.ietf.org/html/draft-behera-ldap-password-policy-10 ControlTypeBeheraPasswordPolicy = "1.3.6.1.4.1.42.2.27.8.5.1" // ControlTypeVChuPasswordMustChange - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4" // ControlTypeVChuPasswordWarning - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5" // ControlTypeManageDsaIT - https://tools.ietf.org/html/rfc3296 ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2" // ControlTypeWhoAmI - https://tools.ietf.org/html/rfc4532 ControlTypeWhoAmI = "1.3.6.1.4.1.4203.1.11.3" // ControlTypeSubtreeDelete - https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 ControlTypeSubtreeDelete = "1.2.840.113556.1.4.805" // ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt ControlTypeServerSideSorting = "1.2.840.113556.1.4.473" // ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt ControlTypeServerSideSortingResult = "1.2.840.113556.1.4.474" // ControlTypeMicrosoftNotification - https://msdn.microsoft.com/en-us/library/aa366983(v=vs.85).aspx ControlTypeMicrosoftNotification = "1.2.840.113556.1.4.528" // ControlTypeMicrosoftShowDeleted - https://msdn.microsoft.com/en-us/library/aa366989(v=vs.85).aspx ControlTypeMicrosoftShowDeleted = "1.2.840.113556.1.4.417" // ControlTypeMicrosoftServerLinkTTL - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309" // ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx ControlTypeDirSync = "1.2.840.113556.1.4.841" // ControlTypeSyncRequest - https://www.ietf.org/rfc/rfc4533.txt ControlTypeSyncRequest = "1.3.6.1.4.1.4203.1.9.1.1" // ControlTypeSyncState - https://www.ietf.org/rfc/rfc4533.txt ControlTypeSyncState = "1.3.6.1.4.1.4203.1.9.1.2" // ControlTypeSyncDone - https://www.ietf.org/rfc/rfc4533.txt ControlTypeSyncDone = "1.3.6.1.4.1.4203.1.9.1.3" // ControlTypeSyncInfo - https://www.ietf.org/rfc/rfc4533.txt ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4" ) // Flags for DirSync control const ( DirSyncIncrementalValues int64 = 2147483648 DirSyncPublicDataOnly int64 = 8192 DirSyncAncestorsFirstOrder int64 = 2048 DirSyncObjectSecurity int64 = 1 ) // ControlTypeMap maps controls to text descriptions var ControlTypeMap = map[string]string{ ControlTypePaging: "Paging", ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", ControlTypeManageDsaIT: "Manage DSA IT", ControlTypeSubtreeDelete: "Subtree Delete Control", ControlTypeMicrosoftNotification: "Change Notification - Microsoft", ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft", ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft", ControlTypeServerSideSorting: "Server Side Sorting Request - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)", ControlTypeServerSideSortingResult: "Server Side Sorting Results - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)", ControlTypeDirSync: "DirSync", ControlTypeSyncRequest: "Sync Request", ControlTypeSyncState: "Sync State", ControlTypeSyncDone: "Sync Done", ControlTypeSyncInfo: "Sync Info", } // Control defines an interface controls provide to encode and describe themselves type Control interface { // GetControlType returns the OID GetControlType() string // Encode returns the ber packet representation Encode() *ber.Packet // String returns a human-readable description String() string } // ControlString implements the Control interface for simple controls type ControlString struct { ControlType string Criticality bool ControlValue string } // GetControlType returns the OID func (c *ControlString) GetControlType() string { return c.ControlType } // Encode returns the ber packet representation func (c *ControlString) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlType, "Control Type ("+ControlTypeMap[c.ControlType]+")")) if c.Criticality { packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) } if c.ControlValue != "" { packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(c.ControlValue), "Control Value")) } return packet } // String returns a human-readable description func (c *ControlString) String() string { return fmt.Sprintf("Control Type: %s (%q) Criticality: %t Control Value: %s", ControlTypeMap[c.ControlType], c.ControlType, c.Criticality, c.ControlValue) } // ControlPaging implements the paging control described in https://www.ietf.org/rfc/rfc2696.txt type ControlPaging struct { // PagingSize indicates the page size PagingSize uint32 // Cookie is an opaque value returned by the server to track a paging cursor Cookie []byte } // GetControlType returns the OID func (c *ControlPaging) GetControlType() string { return ControlTypePaging } // Encode returns the ber packet representation func (c *ControlPaging) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypePaging, "Control Type ("+ControlTypeMap[ControlTypePaging]+")")) p2 := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Paging)") seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Search Control Value") seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.PagingSize), "Paging Size")) cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") cookie.Value = c.Cookie cookie.Data.Write(c.Cookie) seq.AppendChild(cookie) p2.AppendChild(seq) packet.AppendChild(p2) return packet } // String returns a human-readable description func (c *ControlPaging) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t PagingSize: %d Cookie: %q", ControlTypeMap[ControlTypePaging], ControlTypePaging, false, c.PagingSize, c.Cookie) } // SetCookie stores the given cookie in the paging control func (c *ControlPaging) SetCookie(cookie []byte) { c.Cookie = cookie } // ControlBeheraPasswordPolicy implements the control described in https://tools.ietf.org/html/draft-behera-ldap-password-policy-10 type ControlBeheraPasswordPolicy struct { // Expire contains the number of seconds before a password will expire Expire int64 // Grace indicates the remaining number of times a user will be allowed to authenticate with an expired password Grace int64 // Error indicates the error code Error int8 // ErrorString is a human readable error ErrorString string } // GetControlType returns the OID func (c *ControlBeheraPasswordPolicy) GetControlType() string { return ControlTypeBeheraPasswordPolicy } // Encode returns the ber packet representation func (c *ControlBeheraPasswordPolicy) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeBeheraPasswordPolicy, "Control Type ("+ControlTypeMap[ControlTypeBeheraPasswordPolicy]+")")) return packet } // String returns a human-readable description func (c *ControlBeheraPasswordPolicy) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t Expire: %d Grace: %d Error: %d, ErrorString: %s", ControlTypeMap[ControlTypeBeheraPasswordPolicy], ControlTypeBeheraPasswordPolicy, false, c.Expire, c.Grace, c.Error, c.ErrorString) } // ControlVChuPasswordMustChange implements the control described in https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 type ControlVChuPasswordMustChange struct { // MustChange indicates if the password is required to be changed MustChange bool } // GetControlType returns the OID func (c *ControlVChuPasswordMustChange) GetControlType() string { return ControlTypeVChuPasswordMustChange } // Encode returns the ber packet representation func (c *ControlVChuPasswordMustChange) Encode() *ber.Packet { return nil } // String returns a human-readable description func (c *ControlVChuPasswordMustChange) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t MustChange: %v", ControlTypeMap[ControlTypeVChuPasswordMustChange], ControlTypeVChuPasswordMustChange, false, c.MustChange) } // ControlVChuPasswordWarning implements the control described in https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 type ControlVChuPasswordWarning struct { // Expire indicates the time in seconds until the password expires Expire int64 } // GetControlType returns the OID func (c *ControlVChuPasswordWarning) GetControlType() string { return ControlTypeVChuPasswordWarning } // Encode returns the ber packet representation func (c *ControlVChuPasswordWarning) Encode() *ber.Packet { return nil } // String returns a human-readable description func (c *ControlVChuPasswordWarning) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t Expire: %b", ControlTypeMap[ControlTypeVChuPasswordWarning], ControlTypeVChuPasswordWarning, false, c.Expire) } // ControlManageDsaIT implements the control described in https://tools.ietf.org/html/rfc3296 type ControlManageDsaIT struct { // Criticality indicates if this control is required Criticality bool } // GetControlType returns the OID func (c *ControlManageDsaIT) GetControlType() string { return ControlTypeManageDsaIT } // Encode returns the ber packet representation func (c *ControlManageDsaIT) Encode() *ber.Packet { // FIXME packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeManageDsaIT, "Control Type ("+ControlTypeMap[ControlTypeManageDsaIT]+")")) if c.Criticality { packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) } return packet } // String returns a human-readable description func (c *ControlManageDsaIT) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t", ControlTypeMap[ControlTypeManageDsaIT], ControlTypeManageDsaIT, c.Criticality) } // NewControlManageDsaIT returns a ControlManageDsaIT control func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT { return &ControlManageDsaIT{Criticality: Criticality} } // ControlMicrosoftNotification implements the control described in https://msdn.microsoft.com/en-us/library/aa366983(v=vs.85).aspx type ControlMicrosoftNotification struct{} // GetControlType returns the OID func (c *ControlMicrosoftNotification) GetControlType() string { return ControlTypeMicrosoftNotification } // Encode returns the ber packet representation func (c *ControlMicrosoftNotification) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeMicrosoftNotification, "Control Type ("+ControlTypeMap[ControlTypeMicrosoftNotification]+")")) return packet } // String returns a human-readable description func (c *ControlMicrosoftNotification) String() string { return fmt.Sprintf( "Control Type: %s (%q)", ControlTypeMap[ControlTypeMicrosoftNotification], ControlTypeMicrosoftNotification) } // NewControlMicrosoftNotification returns a ControlMicrosoftNotification control func NewControlMicrosoftNotification() *ControlMicrosoftNotification { return &ControlMicrosoftNotification{} } // ControlMicrosoftShowDeleted implements the control described in https://msdn.microsoft.com/en-us/library/aa366989(v=vs.85).aspx type ControlMicrosoftShowDeleted struct{} // GetControlType returns the OID func (c *ControlMicrosoftShowDeleted) GetControlType() string { return ControlTypeMicrosoftShowDeleted } // Encode returns the ber packet representation func (c *ControlMicrosoftShowDeleted) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeMicrosoftShowDeleted, "Control Type ("+ControlTypeMap[ControlTypeMicrosoftShowDeleted]+")")) return packet } // String returns a human-readable description func (c *ControlMicrosoftShowDeleted) String() string { return fmt.Sprintf( "Control Type: %s (%q)", ControlTypeMap[ControlTypeMicrosoftShowDeleted], ControlTypeMicrosoftShowDeleted) } // NewControlMicrosoftShowDeleted returns a ControlMicrosoftShowDeleted control func NewControlMicrosoftShowDeleted() *ControlMicrosoftShowDeleted { return &ControlMicrosoftShowDeleted{} } // ControlMicrosoftServerLinkTTL implements the control described in https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN type ControlMicrosoftServerLinkTTL struct{} // GetControlType returns the OID func (c *ControlMicrosoftServerLinkTTL) GetControlType() string { return ControlTypeMicrosoftServerLinkTTL } // Encode returns the ber packet representation func (c *ControlMicrosoftServerLinkTTL) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeMicrosoftServerLinkTTL, "Control Type ("+ControlTypeMap[ControlTypeMicrosoftServerLinkTTL]+")")) return packet } // String returns a human-readable description func (c *ControlMicrosoftServerLinkTTL) String() string { return fmt.Sprintf( "Control Type: %s (%q)", ControlTypeMap[ControlTypeMicrosoftServerLinkTTL], ControlTypeMicrosoftServerLinkTTL) } // NewControlMicrosoftServerLinkTTL returns a ControlMicrosoftServerLinkTTL control func NewControlMicrosoftServerLinkTTL() *ControlMicrosoftServerLinkTTL { return &ControlMicrosoftServerLinkTTL{} } // FindControl returns the first control of the given type in the list, or nil func FindControl(controls []Control, controlType string) Control { for _, c := range controls { if c.GetControlType() == controlType { return c } } return nil } // DecodeControl returns a control read from the given packet, or nil if no recognized control can be made func DecodeControl(packet *ber.Packet) (Control, error) { var ( ControlType = "" Criticality = false value *ber.Packet ) switch len(packet.Children) { case 0: // at least one child is required for control type return nil, fmt.Errorf("at least one child is required for control type") case 1: // just type, no criticality or value packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" ControlType = packet.Children[0].Value.(string) case 2: packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" if packet.Children[0].Value != nil { ControlType = packet.Children[0].Value.(string) } else if packet.Children[0].Data != nil { ControlType = packet.Children[0].Data.String() } else { return nil, fmt.Errorf("not found where to get the control type") } // Children[1] could be criticality or value (both are optional) // duck-type on whether this is a boolean if _, ok := packet.Children[1].Value.(bool); ok { packet.Children[1].Description = "Criticality" Criticality = packet.Children[1].Value.(bool) } else { packet.Children[1].Description = "Control Value" value = packet.Children[1] } case 3: packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" ControlType = packet.Children[0].Value.(string) packet.Children[1].Description = "Criticality" Criticality = packet.Children[1].Value.(bool) packet.Children[2].Description = "Control Value" value = packet.Children[2] default: // more than 3 children is invalid return nil, fmt.Errorf("more than 3 children is invalid for controls") } switch ControlType { case ControlTypeManageDsaIT: return NewControlManageDsaIT(Criticality), nil case ControlTypePaging: value.Description += " (Paging)" c := new(ControlPaging) if value.Value != nil { valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) if err != nil { return nil, fmt.Errorf("failed to decode data bytes: %s", err) } value.Data.Truncate(0) value.Value = nil value.AppendChild(valueChildren) } value = value.Children[0] value.Description = "Search Control Value" value.Children[0].Description = "Paging Size" value.Children[1].Description = "Cookie" c.PagingSize = uint32(value.Children[0].Value.(int64)) c.Cookie = value.Children[1].Data.Bytes() value.Children[1].Value = c.Cookie return c, nil case ControlTypeBeheraPasswordPolicy: value.Description += " (Password Policy - Behera)" c := NewControlBeheraPasswordPolicy() if value.Value != nil { valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) if err != nil { return nil, fmt.Errorf("failed to decode data bytes: %s", err) } value.Data.Truncate(0) value.Value = nil value.AppendChild(valueChildren) } sequence := value.Children[0] for _, child := range sequence.Children { if child.Tag == 0 { // Warning warningPacket := child.Children[0] val, err := ber.ParseInt64(warningPacket.Data.Bytes()) if err != nil { return nil, fmt.Errorf("failed to decode data bytes: %s", err) } if warningPacket.Tag == 0 { // timeBeforeExpiration c.Expire = val warningPacket.Value = c.Expire } else if warningPacket.Tag == 1 { // graceAuthNsRemaining c.Grace = val warningPacket.Value = c.Grace } } else if child.Tag == 1 { // Error bs := child.Data.Bytes() if len(bs) != 1 || bs[0] > 8 { return nil, fmt.Errorf("failed to decode data bytes: %s", "invalid PasswordPolicyResponse enum value") } val := int8(bs[0]) c.Error = val child.Value = c.Error c.ErrorString = BeheraPasswordPolicyErrorMap[c.Error] } } return c, nil case ControlTypeVChuPasswordMustChange: c := &ControlVChuPasswordMustChange{MustChange: true} return c, nil case ControlTypeVChuPasswordWarning: c := &ControlVChuPasswordWarning{Expire: -1} expireStr := ber.DecodeString(value.Data.Bytes()) expire, err := strconv.ParseInt(expireStr, 10, 64) if err != nil { return nil, fmt.Errorf("failed to parse value as int: %s", err) } c.Expire = expire value.Value = c.Expire return c, nil case ControlTypeMicrosoftNotification: return NewControlMicrosoftNotification(), nil case ControlTypeMicrosoftShowDeleted: return NewControlMicrosoftShowDeleted(), nil case ControlTypeMicrosoftServerLinkTTL: return NewControlMicrosoftServerLinkTTL(), nil case ControlTypeSubtreeDelete: return NewControlSubtreeDelete(), nil case ControlTypeServerSideSorting: return NewControlServerSideSorting(value) case ControlTypeServerSideSortingResult: return NewControlServerSideSortingResult(value) case ControlTypeDirSync: value.Description += " (DirSync)" return NewResponseControlDirSync(value) case ControlTypeSyncState: value.Description += " (Sync State)" valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) if err != nil { return nil, fmt.Errorf("failed to decode data bytes: %s", err) } return NewControlSyncState(valueChildren) case ControlTypeSyncDone: value.Description += " (Sync Done)" valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) if err != nil { return nil, fmt.Errorf("failed to decode data bytes: %s", err) } return NewControlSyncDone(valueChildren) case ControlTypeSyncInfo: value.Description += " (Sync Info)" valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) if err != nil { return nil, fmt.Errorf("failed to decode data bytes: %s", err) } return NewControlSyncInfo(valueChildren) default: c := new(ControlString) c.ControlType = ControlType c.Criticality = Criticality if value != nil { c.ControlValue = value.Value.(string) } return c, nil } } // NewControlString returns a generic control func NewControlString(controlType string, criticality bool, controlValue string) *ControlString { return &ControlString{ ControlType: controlType, Criticality: criticality, ControlValue: controlValue, } } // NewControlPaging returns a paging control func NewControlPaging(pagingSize uint32) *ControlPaging { return &ControlPaging{PagingSize: pagingSize} } // NewControlBeheraPasswordPolicy returns a ControlBeheraPasswordPolicy func NewControlBeheraPasswordPolicy() *ControlBeheraPasswordPolicy { return &ControlBeheraPasswordPolicy{ Expire: -1, Grace: -1, Error: -1, } } // ControlSubtreeDelete implements the subtree delete control described in // https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 type ControlSubtreeDelete struct{} // GetControlType returns the OID func (c *ControlSubtreeDelete) GetControlType() string { return ControlTypeSubtreeDelete } // NewControlSubtreeDelete returns a ControlSubtreeDelete control. func NewControlSubtreeDelete() *ControlSubtreeDelete { return &ControlSubtreeDelete{} } // Encode returns the ber packet representation func (c *ControlSubtreeDelete) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSubtreeDelete, "Control Type ("+ControlTypeMap[ControlTypeSubtreeDelete]+")")) return packet } func (c *ControlSubtreeDelete) String() string { return fmt.Sprintf( "Control Type: %s (%q)", ControlTypeMap[ControlTypeSubtreeDelete], ControlTypeSubtreeDelete) } func encodeControls(controls []Control) *ber.Packet { packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") for _, control := range controls { packet.AppendChild(control.Encode()) } return packet } // ControlDirSync implements the control described in https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx type ControlDirSync struct { Criticality bool Flags int64 MaxAttrCount int64 Cookie []byte } // Deprecated: Use NewRequestControlDirSync instead func NewControlDirSync(flags int64, maxAttrCount int64, cookie []byte) *ControlDirSync { return NewRequestControlDirSync(flags, maxAttrCount, cookie) } // NewRequestControlDirSync returns a dir sync control func NewRequestControlDirSync( flags int64, maxAttrCount int64, cookie []byte, ) *ControlDirSync { return &ControlDirSync{ Criticality: true, Flags: flags, MaxAttrCount: maxAttrCount, Cookie: cookie, } } // NewResponseControlDirSync returns a dir sync control func NewResponseControlDirSync(value *ber.Packet) (*ControlDirSync, error) { if value.Value != nil { valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) if err != nil { return nil, fmt.Errorf("failed to decode data bytes: %s", err) } value.Data.Truncate(0) value.Value = nil value.AppendChild(valueChildren) } child := value.Children[0] if len(child.Children) != 3 { // also on initial creation, Cookie is an empty string return nil, fmt.Errorf("invalid number of children in dirSync control") } child.Description = "DirSync Control Value" child.Children[0].Description = "Flags" child.Children[1].Description = "MaxAttrCount" child.Children[2].Description = "Cookie" cookie := child.Children[2].Data.Bytes() child.Children[2].Value = cookie return &ControlDirSync{ Criticality: true, Flags: child.Children[0].Value.(int64), MaxAttrCount: child.Children[1].Value.(int64), Cookie: cookie, }, nil } // GetControlType returns the OID func (c *ControlDirSync) GetControlType() string { return ControlTypeDirSync } // String returns a human-readable description func (c *ControlDirSync) String() string { return fmt.Sprintf( "ControlType: %s (%q) Criticality: %t ControlValue: Flags: %d MaxAttrCount: %d", ControlTypeMap[ControlTypeDirSync], ControlTypeDirSync, c.Criticality, c.Flags, c.MaxAttrCount, ) } // Encode returns the ber packet representation func (c *ControlDirSync) Encode() *ber.Packet { cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Cookie") if len(c.Cookie) != 0 { cookie.Value = c.Cookie cookie.Data.Write(c.Cookie) } packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeDirSync, "Control Type ("+ControlTypeMap[ControlTypeDirSync]+")")) packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) // must be true always val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (DirSync)") seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "DirSync Control Value") seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.Flags), "Flags")) seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.MaxAttrCount), "MaxAttrCount")) seq.AppendChild(cookie) val.AppendChild(seq) packet.AppendChild(val) return packet } // SetCookie stores the given cookie in the dirSync control func (c *ControlDirSync) SetCookie(cookie []byte) { c.Cookie = cookie } // ControlServerSideSorting type SortKey struct { Reverse bool AttributeType string MatchingRule string } type ControlServerSideSorting struct { SortKeys []*SortKey } func (c *ControlServerSideSorting) GetControlType() string { return ControlTypeServerSideSorting } func NewControlServerSideSorting(value *ber.Packet) (*ControlServerSideSorting, error) { sortKeys := []*SortKey{} val := value.Children[1].Children if len(val) != 1 { return nil, fmt.Errorf("no sequence value in packet") } sequences := val[0].Children for i, sequence := range sequences { sortKey := &SortKey{} if len(sequence.Children) < 2 { return nil, fmt.Errorf("attributeType or matchingRule is missing from sequence %d", i) } sortKey.AttributeType = sequence.Children[0].Value.(string) sortKey.MatchingRule = sequence.Children[1].Value.(string) if len(sequence.Children) == 3 { sortKey.Reverse = sequence.Children[2].Value.(bool) } sortKeys = append(sortKeys, sortKey) } return &ControlServerSideSorting{SortKeys: sortKeys}, nil } func NewControlServerSideSortingWithSortKeys(sortKeys []*SortKey) *ControlServerSideSorting { return &ControlServerSideSorting{SortKeys: sortKeys} } func (c *ControlServerSideSorting) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") control := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.GetControlType(), "Control Type") value := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value") seqs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortKeyList") for _, f := range c.SortKeys { seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "") seq.AppendChild( ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, f.AttributeType, "attributeType"), ) seq.AppendChild( ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, f.MatchingRule, "orderingRule"), ) if f.Reverse { seq.AppendChild( ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, 1, f.Reverse, "reverseOrder"), ) } seqs.AppendChild(seq) } value.AppendChild(seqs) packet.AppendChild(control) packet.AppendChild(value) return packet } func (c *ControlServerSideSorting) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality:%t %+v", "Server Side Sorting", c.GetControlType(), false, c.SortKeys, ) } // ControlServerSideSortingResponse const ( ControlServerSideSortingCodeSuccess ControlServerSideSortingCode = 0 ControlServerSideSortingCodeOperationsError ControlServerSideSortingCode = 1 ControlServerSideSortingCodeTimeLimitExceeded ControlServerSideSortingCode = 2 ControlServerSideSortingCodeStrongAuthRequired ControlServerSideSortingCode = 8 ControlServerSideSortingCodeAdminLimitExceeded ControlServerSideSortingCode = 11 ControlServerSideSortingCodeNoSuchAttribute ControlServerSideSortingCode = 16 ControlServerSideSortingCodeInappropriateMatching ControlServerSideSortingCode = 18 ControlServerSideSortingCodeInsufficientAccessRights ControlServerSideSortingCode = 50 ControlServerSideSortingCodeBusy ControlServerSideSortingCode = 51 ControlServerSideSortingCodeUnwillingToPerform ControlServerSideSortingCode = 53 ControlServerSideSortingCodeOther ControlServerSideSortingCode = 80 ) var ControlServerSideSortingCodes = []ControlServerSideSortingCode{ ControlServerSideSortingCodeSuccess, ControlServerSideSortingCodeOperationsError, ControlServerSideSortingCodeTimeLimitExceeded, ControlServerSideSortingCodeStrongAuthRequired, ControlServerSideSortingCodeAdminLimitExceeded, ControlServerSideSortingCodeNoSuchAttribute, ControlServerSideSortingCodeInappropriateMatching, ControlServerSideSortingCodeInsufficientAccessRights, ControlServerSideSortingCodeBusy, ControlServerSideSortingCodeUnwillingToPerform, ControlServerSideSortingCodeOther, } type ControlServerSideSortingCode int64 // Valid test the code contained in the control against the ControlServerSideSortingCodes slice and return an error if the code is unknown. func (c ControlServerSideSortingCode) Valid() error { for _, validRet := range ControlServerSideSortingCodes { if c == validRet { return nil } } return fmt.Errorf("unknown return code : %d", c) } func NewControlServerSideSortingResult(pkt *ber.Packet) (*ControlServerSideSortingResult, error) { control := &ControlServerSideSortingResult{} if pkt == nil || len(pkt.Children) == 0 { return nil, fmt.Errorf("bad packet") } codeInt, err := ber.ParseInt64(pkt.Children[0].Data.Bytes()) if err != nil { return nil, err } code := ControlServerSideSortingCode(codeInt) if err := code.Valid(); err != nil { return nil, err } return control, nil } type ControlServerSideSortingResult struct { Criticality bool Result ControlServerSideSortingCode // Not populated for now. I can't get openldap to send me this value, so I think this is specific to other directory server // AttributeType string } func (control *ControlServerSideSortingResult) GetControlType() string { return ControlTypeServerSideSortingResult } func (c *ControlServerSideSortingResult) Encode() *ber.Packet { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortResult sequence") sortResult := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(c.Result), "SortResult") packet.AppendChild(sortResult) return packet } func (c *ControlServerSideSortingResult) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality:%t ResultCode:%+v", "Server Side Sorting Result", c.GetControlType(), c.Criticality, c.Result, ) } // Mode for ControlTypeSyncRequest type ControlSyncRequestMode int64 const ( SyncRequestModeRefreshOnly ControlSyncRequestMode = 1 SyncRequestModeRefreshAndPersist ControlSyncRequestMode = 3 ) // ControlSyncRequest implements the Sync Request Control described in https://www.ietf.org/rfc/rfc4533.txt type ControlSyncRequest struct { Criticality bool Mode ControlSyncRequestMode Cookie []byte ReloadHint bool } func NewControlSyncRequest( mode ControlSyncRequestMode, cookie []byte, reloadHint bool, ) *ControlSyncRequest { return &ControlSyncRequest{ Criticality: true, Mode: mode, Cookie: cookie, ReloadHint: reloadHint, } } // GetControlType returns the OID func (c *ControlSyncRequest) GetControlType() string { return ControlTypeSyncRequest } // Encode encodes the control func (c *ControlSyncRequest) Encode() *ber.Packet { _mode := int64(c.Mode) mode := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, _mode, "Mode") var cookie *ber.Packet if len(c.Cookie) > 0 { cookie = ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") cookie.Value = c.Cookie cookie.Data.Write(c.Cookie) } reloadHint := ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.ReloadHint, "Reload Hint") packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSyncRequest, "Control Type ("+ControlTypeMap[ControlTypeSyncRequest]+")")) packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Sync Request)") seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Sync Request Value") seq.AppendChild(mode) if cookie != nil { seq.AppendChild(cookie) } seq.AppendChild(reloadHint) val.AppendChild(seq) packet.AppendChild(val) return packet } // String returns a human-readable description func (c *ControlSyncRequest) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t Mode: %d Cookie: %s ReloadHint: %t", ControlTypeMap[ControlTypeSyncRequest], ControlTypeSyncRequest, c.Criticality, c.Mode, string(c.Cookie), c.ReloadHint, ) } // State for ControlSyncState type ControlSyncStateState int64 const ( SyncStatePresent ControlSyncStateState = 0 SyncStateAdd ControlSyncStateState = 1 SyncStateModify ControlSyncStateState = 2 SyncStateDelete ControlSyncStateState = 3 ) // ControlSyncState implements the Sync State Control described in https://www.ietf.org/rfc/rfc4533.txt type ControlSyncState struct { Criticality bool State ControlSyncStateState EntryUUID uuid.UUID Cookie []byte } func NewControlSyncState(pkt *ber.Packet) (*ControlSyncState, error) { var ( state ControlSyncStateState entryUUID uuid.UUID cookie []byte err error ) switch len(pkt.Children) { case 0, 1: return nil, fmt.Errorf("at least two children are required: %d", len(pkt.Children)) case 2: state = ControlSyncStateState(pkt.Children[0].Value.(int64)) entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) if err != nil { return nil, fmt.Errorf("failed to decode uuid: %w", err) } case 3: state = ControlSyncStateState(pkt.Children[0].Value.(int64)) entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) if err != nil { return nil, fmt.Errorf("failed to decode uuid: %w", err) } cookie = pkt.Children[2].ByteValue } return &ControlSyncState{ Criticality: false, State: state, EntryUUID: entryUUID, Cookie: cookie, }, nil } // GetControlType returns the OID func (c *ControlSyncState) GetControlType() string { return ControlTypeSyncState } // Encode encodes the control func (c *ControlSyncState) Encode() *ber.Packet { return nil } // String returns a human-readable description func (c *ControlSyncState) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t State: %d EntryUUID: %s Cookie: %s", ControlTypeMap[ControlTypeSyncState], ControlTypeSyncState, c.Criticality, c.State, c.EntryUUID.String(), string(c.Cookie), ) } // ControlSyncDone implements the Sync Done Control described in https://www.ietf.org/rfc/rfc4533.txt type ControlSyncDone struct { Criticality bool Cookie []byte RefreshDeletes bool } func NewControlSyncDone(pkt *ber.Packet) (*ControlSyncDone, error) { var ( cookie []byte refreshDeletes bool ) switch len(pkt.Children) { case 0: // have nothing to do case 1: cookie = pkt.Children[0].ByteValue case 2: cookie = pkt.Children[0].ByteValue refreshDeletes = pkt.Children[1].Value.(bool) } return &ControlSyncDone{ Criticality: false, Cookie: cookie, RefreshDeletes: refreshDeletes, }, nil } // GetControlType returns the OID func (c *ControlSyncDone) GetControlType() string { return ControlTypeSyncDone } // Encode encodes the control func (c *ControlSyncDone) Encode() *ber.Packet { return nil } // String returns a human-readable description func (c *ControlSyncDone) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t Cookie: %s RefreshDeletes: %t", ControlTypeMap[ControlTypeSyncDone], ControlTypeSyncDone, c.Criticality, string(c.Cookie), c.RefreshDeletes, ) } // Tag For ControlSyncInfo type ControlSyncInfoValue uint64 const ( SyncInfoNewcookie ControlSyncInfoValue = 0 SyncInfoRefreshDelete ControlSyncInfoValue = 1 SyncInfoRefreshPresent ControlSyncInfoValue = 2 SyncInfoSyncIdSet ControlSyncInfoValue = 3 ) // ControlSyncInfoNewCookie implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt type ControlSyncInfoNewCookie struct { Cookie []byte } // String returns a human-readable description func (c *ControlSyncInfoNewCookie) String() string { return fmt.Sprintf( "NewCookie[Cookie: %s]", string(c.Cookie), ) } // ControlSyncInfoRefreshDelete implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt type ControlSyncInfoRefreshDelete struct { Cookie []byte RefreshDone bool } // String returns a human-readable description func (c *ControlSyncInfoRefreshDelete) String() string { return fmt.Sprintf( "RefreshDelete[Cookie: %s RefreshDone: %t]", string(c.Cookie), c.RefreshDone, ) } // ControlSyncInfoRefreshPresent implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt type ControlSyncInfoRefreshPresent struct { Cookie []byte RefreshDone bool } // String returns a human-readable description func (c *ControlSyncInfoRefreshPresent) String() string { return fmt.Sprintf( "RefreshPresent[Cookie: %s RefreshDone: %t]", string(c.Cookie), c.RefreshDone, ) } // ControlSyncInfoSyncIdSet implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt type ControlSyncInfoSyncIdSet struct { Cookie []byte RefreshDeletes bool SyncUUIDs []uuid.UUID } // String returns a human-readable description func (c *ControlSyncInfoSyncIdSet) String() string { return fmt.Sprintf( "SyncIdSet[Cookie: %s RefreshDeletes: %t SyncUUIDs: %v]", string(c.Cookie), c.RefreshDeletes, c.SyncUUIDs, ) } // ControlSyncInfo implements the Sync Info Control described in https://www.ietf.org/rfc/rfc4533.txt type ControlSyncInfo struct { Criticality bool Value ControlSyncInfoValue NewCookie *ControlSyncInfoNewCookie RefreshDelete *ControlSyncInfoRefreshDelete RefreshPresent *ControlSyncInfoRefreshPresent SyncIdSet *ControlSyncInfoSyncIdSet } func NewControlSyncInfo(pkt *ber.Packet) (*ControlSyncInfo, error) { var ( cookie []byte refreshDone = true refreshDeletes bool syncUUIDs []uuid.UUID ) c := &ControlSyncInfo{Criticality: false} switch ControlSyncInfoValue(pkt.Identifier.Tag) { case SyncInfoNewcookie: c.Value = SyncInfoNewcookie c.NewCookie = &ControlSyncInfoNewCookie{ Cookie: pkt.ByteValue, } case SyncInfoRefreshDelete: c.Value = SyncInfoRefreshDelete switch len(pkt.Children) { case 0: // have nothing to do case 1: cookie = pkt.Children[0].ByteValue case 2: cookie = pkt.Children[0].ByteValue refreshDone = pkt.Children[1].Value.(bool) } c.RefreshDelete = &ControlSyncInfoRefreshDelete{ Cookie: cookie, RefreshDone: refreshDone, } case SyncInfoRefreshPresent: c.Value = SyncInfoRefreshPresent switch len(pkt.Children) { case 0: // have nothing to do case 1: cookie = pkt.Children[0].ByteValue case 2: cookie = pkt.Children[0].ByteValue refreshDone = pkt.Children[1].Value.(bool) } c.RefreshPresent = &ControlSyncInfoRefreshPresent{ Cookie: cookie, RefreshDone: refreshDone, } case SyncInfoSyncIdSet: c.Value = SyncInfoSyncIdSet switch len(pkt.Children) { case 0: // have nothing to do case 1: cookie = pkt.Children[0].ByteValue case 2: cookie = pkt.Children[0].ByteValue refreshDeletes = pkt.Children[1].Value.(bool) case 3: cookie = pkt.Children[0].ByteValue refreshDeletes = pkt.Children[1].Value.(bool) syncUUIDs = make([]uuid.UUID, 0, len(pkt.Children[2].Children)) for _, child := range pkt.Children[2].Children { u, err := uuid.FromBytes(child.ByteValue) if err != nil { return nil, fmt.Errorf("failed to decode uuid: %w", err) } syncUUIDs = append(syncUUIDs, u) } } c.SyncIdSet = &ControlSyncInfoSyncIdSet{ Cookie: cookie, RefreshDeletes: refreshDeletes, SyncUUIDs: syncUUIDs, } default: return nil, fmt.Errorf("unknown sync info value: %d", pkt.Identifier.Tag) } return c, nil } // GetControlType returns the OID func (c *ControlSyncInfo) GetControlType() string { return ControlTypeSyncInfo } // Encode encodes the control func (c *ControlSyncInfo) Encode() *ber.Packet { return nil } // String returns a human-readable description func (c *ControlSyncInfo) String() string { return fmt.Sprintf( "Control Type: %s (%q) Criticality: %t Value: %d %s %s %s %s", ControlTypeMap[ControlTypeSyncInfo], ControlTypeSyncInfo, c.Criticality, c.Value, c.NewCookie, c.RefreshDelete, c.RefreshPresent, c.SyncIdSet, ) }
package ldap import ( ber "github.com/go-asn1-ber/asn1-ber" ) // debugging type // - has a Printf method to write the debug output type debugging bool // Enable controls debugging mode. func (debug *debugging) Enable(b bool) { *debug = debugging(b) } // Printf writes debug output. func (debug debugging) Printf(format string, args ...interface{}) { if debug { logger.Printf(format, args...) } } // PrintPacket dumps a packet. func (debug debugging) PrintPacket(packet *ber.Packet) { if debug { ber.WritePacket(logger.Writer(), packet) } }
package ldap import ( "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) // DelRequest implements an LDAP deletion request type DelRequest struct { // DN is the name of the directory entry to delete DN string // Controls hold optional controls to send with the request Controls []Control } func (req *DelRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationDelRequest, req.DN, "Del Request") pkt.Data.Write([]byte(req.DN)) envelope.AppendChild(pkt) if len(req.Controls) > 0 { envelope.AppendChild(encodeControls(req.Controls)) } return nil } // NewDelRequest creates a delete request for the given DN and controls func NewDelRequest(DN string, Controls []Control) *DelRequest { return &DelRequest{ DN: DN, Controls: Controls, } } // Del executes the given delete request func (l *Conn) Del(delRequest *DelRequest) error { msgCtx, err := l.doRequest(delRequest) if err != nil { return err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return err } if packet.Children[1].Tag == ApplicationDelResponse { err := GetLDAPError(packet) if err != nil { return err } } else { return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) } return nil }
package ldap import ( "encoding/hex" "errors" "fmt" ber "github.com/go-asn1-ber/asn1-ber" "sort" "strings" "unicode" "unicode/utf8" ) // AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514 type AttributeTypeAndValue struct { // Type is the attribute type Type string // Value is the attribute value Value string } func (a *AttributeTypeAndValue) setType(str string) error { result, err := decodeString(str) if err != nil { return err } a.Type = result return nil } func (a *AttributeTypeAndValue) setValue(s string) error { // https://www.ietf.org/rfc/rfc4514.html#section-2.4 // If the AttributeType is of the dotted-decimal form, the // AttributeValue is represented by an number sign ('#' U+0023) // character followed by the hexadecimal encoding of each of the octets // of the BER encoding of the X.500 AttributeValue. if len(s) > 0 && s[0] == '#' { decodedString, err := decodeEncodedString(s[1:]) if err != nil { return err } a.Value = decodedString return nil } else { decodedString, err := decodeString(s) if err != nil { return err } a.Value = decodedString return nil } } // String returns a normalized string representation of this attribute type and // value pair which is the lowercase join of the Type and Value with a "=". func (a *AttributeTypeAndValue) String() string { return encodeString(foldString(a.Type), false) + "=" + encodeString(a.Value, true) } // 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, ",") } func stripLeadingAndTrailingSpaces(inVal string) string { noSpaces := strings.Trim(inVal, " ") // Re-add the trailing space if it was an escaped space if len(noSpaces) > 0 && noSpaces[len(noSpaces)-1] == '\\' && inVal[len(inVal)-1] == ' ' { noSpaces = noSpaces + " " } return noSpaces } // Remove leading and trailing spaces from the attribute type and value // and unescape any escaped characters in these fields // // decodeString is based on https://github.com/inteon/cert-manager/blob/ed280d28cd02b262c5db46054d88e70ab518299c/pkg/util/pki/internal/dn.go#L170 func decodeString(str string) (string, error) { s := []rune(stripLeadingAndTrailingSpaces(str)) builder := strings.Builder{} for i := 0; i < len(s); i++ { char := s[i] // If the character is not an escape character, just add it to the // builder and continue if char != '\\' { builder.WriteRune(char) continue } // If the escape character is the last character, it's a corrupted // escaped character if i+1 >= len(s) { return "", fmt.Errorf("got corrupted escaped character: '%s'", string(s)) } // If the escaped character is a special character, just add it to // the builder and continue switch s[i+1] { case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\': builder.WriteRune(s[i+1]) i++ continue } // If the escaped character is not a special character, it should // be a hex-encoded character of the form \XX if it's not at least // two characters long, it's a corrupted escaped character if i+2 >= len(s) { return "", errors.New("failed to decode escaped character: encoding/hex: invalid byte: " + string(s[i+1])) } // Get the runes for the two characters after the escape character // and convert them to a byte slice xx := []byte(string(s[i+1 : i+3])) // If the two runes are not hex characters and result in more than // two bytes when converted to a byte slice, it's a corrupted // escaped character if len(xx) != 2 { return "", errors.New("failed to decode escaped character: invalid byte: " + string(xx)) } // Decode the hex-encoded character and add it to the builder dst := []byte{0} if n, err := hex.Decode(dst, xx); err != nil { return "", errors.New("failed to decode escaped character: " + err.Error()) } else if n != 1 { return "", fmt.Errorf("failed to decode escaped character: encoding/hex: expected 1 byte when un-escaping, got %d", n) } builder.WriteByte(dst[0]) i += 2 } return builder.String(), nil } // Escape a string according to RFC 4514 func encodeString(value string, isValue bool) string { builder := strings.Builder{} escapeChar := func(c byte) { builder.WriteByte('\\') builder.WriteByte(c) } escapeHex := func(c byte) { builder.WriteByte('\\') builder.WriteString(hex.EncodeToString([]byte{c})) } // Loop through each byte and escape as necessary. // Runes that take up more than one byte are escaped // byte by byte (since both bytes are non-ASCII). 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 !isValue && char == '=' { // Equal signs have to be escaped only in the type part of // the attribute type and value pair. 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. builder.WriteByte(char) } return builder.String() } func decodeEncodedString(str string) (string, error) { decoded, err := hex.DecodeString(str) if err != nil { return "", fmt.Errorf("failed to decode BER encoding: %w", err) } packet, err := ber.DecodePacketErr(decoded) if err != nil { return "", fmt.Errorf("failed to decode BER encoding: %w", err) } return packet.Data.String(), nil } // ParseDN returns a distinguishedName or an error. // The function respects https://tools.ietf.org/html/rfc4514 func ParseDN(str string) (*DN, error) { var dn = &DN{RDNs: make([]*RelativeDN, 0)} if strings.TrimSpace(str) == "" { return dn, nil } var ( rdn = &RelativeDN{} attr = &AttributeTypeAndValue{} escaping bool startPos int appendAttributesToRDN = func(end bool) { rdn.Attributes = append(rdn.Attributes, attr) attr = &AttributeTypeAndValue{} if end { dn.RDNs = append(dn.RDNs, rdn) rdn = &RelativeDN{} } } ) // Loop through each character in the string and // build up the attribute type and value pairs. // We only check for ascii characters here, which // allows us to iterate over the string byte by byte. for i := 0; i < len(str); i++ { char := str[i] switch { case escaping: escaping = false case char == '\\': escaping = true case char == '=' && len(attr.Type) == 0: if err := attr.setType(str[startPos:i]); err != nil { return nil, err } startPos = i + 1 case char == ',' || char == '+' || char == ';': if len(attr.Type) == 0 { return dn, errors.New("incomplete type, value pair") } if err := attr.setValue(str[startPos:i]); err != nil { return nil, err } startPos = i + 1 last := char == ',' || char == ';' appendAttributesToRDN(last) } } if len(attr.Type) == 0 { return dn, errors.New("DN ended with incomplete type, value pair") } if err := attr.setValue(str[startPos:]); err != nil { return dn, err } appendAttributesToRDN(true) return dn, nil } // Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). // Returns true if they have the same number of relative distinguished names // and corresponding relative distinguished names (by position) are the same. func (d *DN) Equal(other *DN) bool { if len(d.RDNs) != len(other.RDNs) { return false } for i := range d.RDNs { if !d.RDNs[i].Equal(other.RDNs[i]) { return false } } return true } // AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. // "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com" // "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com" // "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com" func (d *DN) AncestorOf(other *DN) bool { if len(d.RDNs) >= len(other.RDNs) { return false } // Take the last `len(d.RDNs)` RDNs from the other DN to compare against otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] for i := range d.RDNs { if !d.RDNs[i].Equal(otherRDNs[i]) { return false } } return true } // Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). // Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues // and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type. // The order of attributes is not significant. // Case of attribute types is not significant. func (r *RelativeDN) Equal(other *RelativeDN) bool { if len(r.Attributes) != len(other.Attributes) { return false } return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes) } func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool { for _, attr := range attrs { found := false for _, myattr := range r.Attributes { if myattr.Equal(attr) { found = true break } } if !found { return false } } return true } // Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue // Case of the attribute type is not significant func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool { return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value } // EqualFold returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). // Returns true if they have the same number of relative distinguished names // and corresponding relative distinguished names (by position) are the same. // Case of the attribute type and value is not significant func (d *DN) EqualFold(other *DN) bool { if len(d.RDNs) != len(other.RDNs) { return false } for i := range d.RDNs { if !d.RDNs[i].EqualFold(other.RDNs[i]) { return false } } return true } // AncestorOfFold returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. // Case of the attribute type and value is not significant func (d *DN) AncestorOfFold(other *DN) bool { if len(d.RDNs) >= len(other.RDNs) { return false } // Take the last `len(d.RDNs)` RDNs from the other DN to compare against otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] for i := range d.RDNs { if !d.RDNs[i].EqualFold(otherRDNs[i]) { return false } } return true } // EqualFold returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). // Case of the attribute type is not significant func (r *RelativeDN) EqualFold(other *RelativeDN) bool { if len(r.Attributes) != len(other.Attributes) { return false } return r.hasAllAttributesFold(other.Attributes) && other.hasAllAttributesFold(r.Attributes) } func (r *RelativeDN) hasAllAttributesFold(attrs []*AttributeTypeAndValue) bool { for _, attr := range attrs { found := false for _, myattr := range r.Attributes { if myattr.EqualFold(attr) { found = true break } } if !found { return false } } return true } // EqualFold returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue // Case of the attribute type and value is not significant func (a *AttributeTypeAndValue) EqualFold(other *AttributeTypeAndValue) bool { return strings.EqualFold(a.Type, other.Type) && strings.EqualFold(a.Value, other.Value) } // foldString returns a folded string such that foldString(x) == foldString(y) // is identical to bytes.EqualFold(x, y). // based on https://go.dev/src/encoding/json/fold.go func foldString(s string) string { builder := strings.Builder{} for _, char := range s { // Handle single-byte ASCII. if char < utf8.RuneSelf { if 'A' <= char && char <= 'Z' { char += 'a' - 'A' } builder.WriteRune(char) continue } builder.WriteRune(foldRune(char)) } return builder.String() } // foldRune is returns the smallest rune for all runes in the same fold set. func foldRune(r rune) rune { for { r2 := unicode.SimpleFold(r) if r2 <= r { return r } r = r2 } }
package ldap import ( "errors" "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) // LDAP Result Codes const ( LDAPResultSuccess = 0 LDAPResultOperationsError = 1 LDAPResultProtocolError = 2 LDAPResultTimeLimitExceeded = 3 LDAPResultSizeLimitExceeded = 4 LDAPResultCompareFalse = 5 LDAPResultCompareTrue = 6 LDAPResultAuthMethodNotSupported = 7 LDAPResultStrongAuthRequired = 8 LDAPResultReferral = 10 LDAPResultAdminLimitExceeded = 11 LDAPResultUnavailableCriticalExtension = 12 LDAPResultConfidentialityRequired = 13 LDAPResultSaslBindInProgress = 14 LDAPResultNoSuchAttribute = 16 LDAPResultUndefinedAttributeType = 17 LDAPResultInappropriateMatching = 18 LDAPResultConstraintViolation = 19 LDAPResultAttributeOrValueExists = 20 LDAPResultInvalidAttributeSyntax = 21 LDAPResultNoSuchObject = 32 LDAPResultAliasProblem = 33 LDAPResultInvalidDNSyntax = 34 LDAPResultIsLeaf = 35 LDAPResultAliasDereferencingProblem = 36 LDAPResultInappropriateAuthentication = 48 LDAPResultInvalidCredentials = 49 LDAPResultInsufficientAccessRights = 50 LDAPResultBusy = 51 LDAPResultUnavailable = 52 LDAPResultUnwillingToPerform = 53 LDAPResultLoopDetect = 54 LDAPResultSortControlMissing = 60 LDAPResultOffsetRangeError = 61 LDAPResultNamingViolation = 64 LDAPResultObjectClassViolation = 65 LDAPResultNotAllowedOnNonLeaf = 66 LDAPResultNotAllowedOnRDN = 67 LDAPResultEntryAlreadyExists = 68 LDAPResultObjectClassModsProhibited = 69 LDAPResultResultsTooLarge = 70 LDAPResultAffectsMultipleDSAs = 71 LDAPResultVirtualListViewErrorOrControlError = 76 LDAPResultOther = 80 LDAPResultServerDown = 81 LDAPResultLocalError = 82 LDAPResultEncodingError = 83 LDAPResultDecodingError = 84 LDAPResultTimeout = 85 LDAPResultAuthUnknown = 86 LDAPResultFilterError = 87 LDAPResultUserCanceled = 88 LDAPResultParamError = 89 LDAPResultNoMemory = 90 LDAPResultConnectError = 91 LDAPResultNotSupported = 92 LDAPResultControlNotFound = 93 LDAPResultNoResultsReturned = 94 LDAPResultMoreResultsToReturn = 95 LDAPResultClientLoop = 96 LDAPResultReferralLimitExceeded = 97 LDAPResultInvalidResponse = 100 LDAPResultAmbiguousResponse = 101 LDAPResultTLSNotSupported = 112 LDAPResultIntermediateResponse = 113 LDAPResultUnknownType = 114 LDAPResultCanceled = 118 LDAPResultNoSuchOperation = 119 LDAPResultTooLate = 120 LDAPResultCannotCancel = 121 LDAPResultAssertionFailed = 122 LDAPResultAuthorizationDenied = 123 LDAPResultSyncRefreshRequired = 4096 ErrorNetwork = 200 ErrorFilterCompile = 201 ErrorFilterDecompile = 202 ErrorDebugging = 203 ErrorUnexpectedMessage = 204 ErrorUnexpectedResponse = 205 ErrorEmptyPassword = 206 ) // LDAPResultCodeMap contains string descriptions for LDAP error codes var LDAPResultCodeMap = map[uint16]string{ LDAPResultSuccess: "Success", LDAPResultOperationsError: "Operations Error", LDAPResultProtocolError: "Protocol Error", LDAPResultTimeLimitExceeded: "Time Limit Exceeded", LDAPResultSizeLimitExceeded: "Size Limit Exceeded", LDAPResultCompareFalse: "Compare False", LDAPResultCompareTrue: "Compare True", LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", LDAPResultStrongAuthRequired: "Strong Auth Required", LDAPResultReferral: "Referral", LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", LDAPResultConfidentialityRequired: "Confidentiality Required", LDAPResultSaslBindInProgress: "Sasl Bind In Progress", LDAPResultNoSuchAttribute: "No Such Attribute", LDAPResultUndefinedAttributeType: "Undefined Attribute Type", LDAPResultInappropriateMatching: "Inappropriate Matching", LDAPResultConstraintViolation: "Constraint Violation", LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", LDAPResultNoSuchObject: "No Such Object", LDAPResultAliasProblem: "Alias Problem", LDAPResultInvalidDNSyntax: "Invalid DN Syntax", LDAPResultIsLeaf: "Is Leaf", LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", LDAPResultInappropriateAuthentication: "Inappropriate Authentication", LDAPResultInvalidCredentials: "Invalid Credentials", LDAPResultInsufficientAccessRights: "Insufficient Access Rights", LDAPResultBusy: "Busy", LDAPResultUnavailable: "Unavailable", LDAPResultUnwillingToPerform: "Unwilling To Perform", LDAPResultLoopDetect: "Loop Detect", LDAPResultSortControlMissing: "Sort Control Missing", LDAPResultOffsetRangeError: "Result Offset Range Error", LDAPResultNamingViolation: "Naming Violation", LDAPResultObjectClassViolation: "Object Class Violation", LDAPResultResultsTooLarge: "Results Too Large", LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", LDAPResultEntryAlreadyExists: "Entry Already Exists", LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", LDAPResultVirtualListViewErrorOrControlError: "Failed because of a problem related to the virtual list view", LDAPResultOther: "Other", LDAPResultServerDown: "Cannot establish a connection", LDAPResultLocalError: "An error occurred", LDAPResultEncodingError: "LDAP encountered an error while encoding", LDAPResultDecodingError: "LDAP encountered an error while decoding", LDAPResultTimeout: "LDAP timeout while waiting for a response from the server", LDAPResultAuthUnknown: "The auth method requested in a bind request is unknown", LDAPResultFilterError: "An error occurred while encoding the given search filter", LDAPResultUserCanceled: "The user canceled the operation", LDAPResultParamError: "An invalid parameter was specified", LDAPResultNoMemory: "Out of memory error", LDAPResultConnectError: "A connection to the server could not be established", LDAPResultNotSupported: "An attempt has been made to use a feature not supported LDAP", LDAPResultControlNotFound: "The controls required to perform the requested operation were not found", LDAPResultNoResultsReturned: "No results were returned from the server", LDAPResultMoreResultsToReturn: "There are more results in the chain of results", LDAPResultClientLoop: "A loop has been detected. For example when following referrals", LDAPResultReferralLimitExceeded: "The referral hop limit has been exceeded", LDAPResultCanceled: "Operation was canceled", LDAPResultNoSuchOperation: "Server has no knowledge of the operation requested for cancellation", LDAPResultTooLate: "Too late to cancel the outstanding operation", LDAPResultCannotCancel: "The identified operation does not support cancellation or the cancel operation cannot be performed", LDAPResultAssertionFailed: "An assertion control given in the LDAP operation evaluated to false causing the operation to not be performed", LDAPResultSyncRefreshRequired: "Refresh Required", LDAPResultInvalidResponse: "Invalid Response", LDAPResultAmbiguousResponse: "Ambiguous Response", LDAPResultTLSNotSupported: "Tls Not Supported", LDAPResultIntermediateResponse: "Intermediate Response", LDAPResultUnknownType: "Unknown Type", LDAPResultAuthorizationDenied: "Authorization Denied", ErrorNetwork: "Network Error", ErrorFilterCompile: "Filter Compile Error", ErrorFilterDecompile: "Filter Decompile Error", ErrorDebugging: "Debugging Error", ErrorUnexpectedMessage: "Unexpected Message", ErrorUnexpectedResponse: "Unexpected Response", ErrorEmptyPassword: "Empty password not allowed by the client", } // Error holds LDAP error information type Error struct { // Err is the underlying error Err error // ResultCode is the LDAP error code ResultCode uint16 // MatchedDN is the matchedDN returned if any MatchedDN string // Packet is the returned packet if any Packet *ber.Packet } func (e *Error) Error() string { return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) } func (e *Error) Unwrap() error { return e.Err } // GetLDAPError creates an Error out of a BER packet representing a LDAPResult // The return is an error object. It can be casted to a Error structure. // This function returns nil if resultCode in the LDAPResult sequence is success(0). func GetLDAPError(packet *ber.Packet) error { if packet == nil { return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty packet")} } if len(packet.Children) >= 2 { response := packet.Children[1] if response == nil { return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty response in packet"), Packet: packet} } if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { if ber.Type(response.Children[0].Tag) == ber.Type(ber.TagInteger) || ber.Type(response.Children[0].Tag) == ber.Type(ber.TagEnumerated) { resultCode := uint16(response.Children[0].Value.(int64)) if resultCode == 0 { // No error return nil } if ber.Type(response.Children[1].Tag) == ber.Type(ber.TagOctetString) && ber.Type(response.Children[2].Tag) == ber.Type(ber.TagOctetString) { return &Error{ ResultCode: resultCode, MatchedDN: response.Children[1].Value.(string), Err: fmt.Errorf("%v", response.Children[2].Value), Packet: packet, } } } } } return &Error{ResultCode: ErrorNetwork, Err: fmt.Errorf("Invalid packet format"), Packet: packet} } // NewError creates an LDAP error with the given code and underlying error func NewError(resultCode uint16, err error) error { return &Error{ResultCode: resultCode, Err: err} } // IsErrorAnyOf returns true if the given error is an LDAP error with any one of the given result codes func IsErrorAnyOf(err error, codes ...uint16) bool { if err == nil { return false } var serverError *Error if !errors.As(err, &serverError) { return false } for _, code := range codes { if serverError.ResultCode == code { return true } } return false } // IsErrorWithCode returns true if the given error is an LDAP error with the given result code func IsErrorWithCode(err error, desiredResultCode uint16) bool { return IsErrorAnyOf(err, desiredResultCode) }
package ldap import ( "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) // ExtendedRequest represents an extended request to send to the server // See: https://www.rfc-editor.org/rfc/rfc4511#section-4.12 type ExtendedRequest struct { // ExtendedRequest ::= [APPLICATION 23] SEQUENCE { // requestName [0] LDAPOID, // requestValue [1] OCTET STRING OPTIONAL } Name string Value *ber.Packet Controls []Control } // NewExtendedRequest returns a new ExtendedRequest. The value can be // nil depending on the type of request func NewExtendedRequest(name string, value *ber.Packet) *ExtendedRequest { return &ExtendedRequest{ Name: name, Value: value, } } func (er ExtendedRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Extended Request") pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, ber.TagEOC, er.Name, "Extended Request Name")) if er.Value != nil { pkt.AppendChild(er.Value) } envelope.AppendChild(pkt) if len(er.Controls) > 0 { envelope.AppendChild(encodeControls(er.Controls)) } return nil } // ExtendedResponse represents the response from the directory server // after sending an extended request // See: https://www.rfc-editor.org/rfc/rfc4511#section-4.12 type ExtendedResponse struct { // ExtendedResponse ::= [APPLICATION 24] SEQUENCE { // COMPONENTS OF LDAPResult, // responseName [10] LDAPOID OPTIONAL, // responseValue [11] OCTET STRING OPTIONAL } Name string Value *ber.Packet Controls []Control } // Extended performs an extended request. The resulting // ExtendedResponse may return a value in the form of a *ber.Packet func (l *Conn) Extended(er *ExtendedRequest) (*ExtendedResponse, error) { msgCtx, err := l.doRequest(er) if err != nil { return nil, err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return nil, err } if err = GetLDAPError(packet); err != nil { return nil, err } if len(packet.Children[1].Children) < 4 { return nil, fmt.Errorf( "ldap: malformed extended response: expected 4 children, got %d", len(packet.Children), ) } response := &ExtendedResponse{ Name: packet.Children[1].Children[3].Data.String(), Controls: make([]Control, 0), } if len(packet.Children) == 3 { for _, child := range packet.Children[2].Children { decodedChild, decodeErr := DecodeControl(child) if decodeErr != nil { return nil, fmt.Errorf("failed to decode child control: %s", decodeErr) } response.Controls = append(response.Controls, decodedChild) } } if len(packet.Children[1].Children) == 5 { response.Value = packet.Children[1].Children[4] } return response, nil }
package ldap import ( "bytes" hexpac "encoding/hex" "errors" "fmt" "io" "strings" "unicode" "unicode/utf8" ber "github.com/go-asn1-ber/asn1-ber" ) // Filter choices const ( FilterAnd = 0 FilterOr = 1 FilterNot = 2 FilterEqualityMatch = 3 FilterSubstrings = 4 FilterGreaterOrEqual = 5 FilterLessOrEqual = 6 FilterPresent = 7 FilterApproxMatch = 8 FilterExtensibleMatch = 9 ) // FilterMap contains human readable descriptions of Filter choices var FilterMap = map[uint64]string{ FilterAnd: "And", FilterOr: "Or", FilterNot: "Not", FilterEqualityMatch: "Equality Match", FilterSubstrings: "Substrings", FilterGreaterOrEqual: "Greater Or Equal", FilterLessOrEqual: "Less Or Equal", FilterPresent: "Present", FilterApproxMatch: "Approx Match", FilterExtensibleMatch: "Extensible Match", } // SubstringFilter options const ( FilterSubstringsInitial = 0 FilterSubstringsAny = 1 FilterSubstringsFinal = 2 ) // FilterSubstringsMap contains human readable descriptions of SubstringFilter choices var FilterSubstringsMap = map[uint64]string{ FilterSubstringsInitial: "Substrings Initial", FilterSubstringsAny: "Substrings Any", FilterSubstringsFinal: "Substrings Final", } // MatchingRuleAssertion choices const ( MatchingRuleAssertionMatchingRule = 1 MatchingRuleAssertionType = 2 MatchingRuleAssertionMatchValue = 3 MatchingRuleAssertionDNAttributes = 4 ) // MatchingRuleAssertionMap contains human readable descriptions of MatchingRuleAssertion choices var MatchingRuleAssertionMap = map[uint64]string{ MatchingRuleAssertionMatchingRule: "Matching Rule Assertion Matching Rule", MatchingRuleAssertionType: "Matching Rule Assertion Type", MatchingRuleAssertionMatchValue: "Matching Rule Assertion Match Value", MatchingRuleAssertionDNAttributes: "Matching Rule Assertion DN Attributes", } var _SymbolAny = []byte{'*'} // CompileFilter converts a string representation of a filter into a BER-encoded packet func CompileFilter(filter string) (*ber.Packet, error) { if len(filter) == 0 || filter[0] != '(' { return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('")) } packet, pos, err := compileFilter(filter, 1) if err != nil { return nil, err } switch { case pos > len(filter): return nil, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) case pos < len(filter): return nil, NewError(ErrorFilterCompile, errors.New("ldap: finished compiling filter with extra at end: "+fmt.Sprint(filter[pos:]))) } return packet, nil } // DecompileFilter converts a packet representation of a filter into a string representation func DecompileFilter(packet *ber.Packet) (_ string, err error) { defer func() { if r := recover(); r != nil { err = NewError(ErrorFilterDecompile, errors.New("ldap: error decompiling filter")) } }() buf := bytes.NewBuffer(nil) buf.WriteByte('(') childStr := "" switch packet.Tag { case FilterAnd: buf.WriteByte('&') for _, child := range packet.Children { childStr, err = DecompileFilter(child) if err != nil { return } buf.WriteString(childStr) } case FilterOr: buf.WriteByte('|') for _, child := range packet.Children { childStr, err = DecompileFilter(child) if err != nil { return } buf.WriteString(childStr) } case FilterNot: buf.WriteByte('!') childStr, err = DecompileFilter(packet.Children[0]) if err != nil { return } buf.WriteString(childStr) case FilterSubstrings: buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) buf.WriteByte('=') for i, child := range packet.Children[1].Children { if i == 0 && child.Tag != FilterSubstringsInitial { buf.Write(_SymbolAny) } buf.WriteString(EscapeFilter(ber.DecodeString(child.Data.Bytes()))) if child.Tag != FilterSubstringsFinal { buf.Write(_SymbolAny) } } case FilterEqualityMatch: buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) buf.WriteByte('=') buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))) case FilterGreaterOrEqual: buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) buf.WriteString(">=") buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))) case FilterLessOrEqual: buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) buf.WriteString("<=") buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))) case FilterPresent: buf.WriteString(ber.DecodeString(packet.Data.Bytes())) buf.WriteString("=*") case FilterApproxMatch: buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) buf.WriteString("~=") buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))) case FilterExtensibleMatch: attr := "" dnAttributes := false matchingRule := "" value := "" for _, child := range packet.Children { switch child.Tag { case MatchingRuleAssertionMatchingRule: matchingRule = ber.DecodeString(child.Data.Bytes()) case MatchingRuleAssertionType: attr = ber.DecodeString(child.Data.Bytes()) case MatchingRuleAssertionMatchValue: value = ber.DecodeString(child.Data.Bytes()) case MatchingRuleAssertionDNAttributes: dnAttributes = child.Value.(bool) } } if len(attr) > 0 { buf.WriteString(attr) } if dnAttributes { buf.WriteString(":dn") } if len(matchingRule) > 0 { buf.WriteString(":") buf.WriteString(matchingRule) } buf.WriteString(":=") buf.WriteString(EscapeFilter(value)) } buf.WriteByte(')') return buf.String(), nil } func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { for pos < len(filter) && filter[pos] == '(' { child, newPos, err := compileFilter(filter, pos+1) if err != nil { return pos, err } pos = newPos parent.AppendChild(child) } if pos == len(filter) { return pos, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) } return pos + 1, nil } func compileFilter(filter string, pos int) (*ber.Packet, int, error) { var ( packet *ber.Packet err error ) defer func() { if r := recover(); r != nil { err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter")) } }() newPos := pos currentRune, currentWidth := utf8.DecodeRuneInString(filter[newPos:]) switch currentRune { case utf8.RuneError: return nil, 0, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) case '(': packet, newPos, err = compileFilter(filter, pos+currentWidth) newPos++ return packet, newPos, err case '&': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd]) newPos, err = compileFilterSet(filter, pos+currentWidth, packet) return packet, newPos, err case '|': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr]) newPos, err = compileFilterSet(filter, pos+currentWidth, packet) return packet, newPos, err case '!': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot]) var child *ber.Packet child, newPos, err = compileFilter(filter, pos+currentWidth) packet.AppendChild(child) return packet, newPos, err default: const ( stateReadingAttr = 0 stateReadingExtensibleMatchingRule = 1 stateReadingCondition = 2 ) state := stateReadingAttr attribute := bytes.NewBuffer(nil) extensibleDNAttributes := false extensibleMatchingRule := bytes.NewBuffer(nil) condition := bytes.NewBuffer(nil) for newPos < len(filter) { remainingFilter := filter[newPos:] currentRune, currentWidth = utf8.DecodeRuneInString(remainingFilter) if currentRune == ')' { break } if currentRune == utf8.RuneError { return packet, newPos, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) } switch state { case stateReadingAttr: switch { // Extensible rule, with only DN-matching case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:="): packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) extensibleDNAttributes = true state = stateReadingCondition newPos += 5 // Extensible rule, with DN-matching and a matching OID case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:"): packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) extensibleDNAttributes = true state = stateReadingExtensibleMatchingRule newPos += 4 // Extensible rule, with attr only case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) state = stateReadingCondition newPos += 2 // Extensible rule, with no DN attribute matching case currentRune == ':': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) state = stateReadingExtensibleMatchingRule newPos++ // Equality condition case currentRune == '=': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) state = stateReadingCondition newPos++ // Greater-than or equal case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="): packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) state = stateReadingCondition newPos += 2 // Less-than or equal case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="): packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) state = stateReadingCondition newPos += 2 // Approx case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="): packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch]) state = stateReadingCondition newPos += 2 // Still reading the attribute name default: attribute.WriteRune(currentRune) newPos += currentWidth } case stateReadingExtensibleMatchingRule: switch { // Matching rule OID is done case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): state = stateReadingCondition newPos += 2 // Still reading the matching rule oid default: extensibleMatchingRule.WriteRune(currentRune) newPos += currentWidth } case stateReadingCondition: // append to the condition condition.WriteRune(currentRune) newPos += currentWidth } } if newPos == len(filter) { err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) return packet, newPos, err } if packet == nil { err = NewError(ErrorFilterCompile, errors.New("ldap: error parsing filter")) return packet, newPos, err } switch { case packet.Tag == FilterExtensibleMatch: // MatchingRuleAssertion ::= SEQUENCE { // matchingRule [1] MatchingRuleID OPTIONAL, // type [2] AttributeDescription OPTIONAL, // matchValue [3] AssertionValue, // dnAttributes [4] BOOLEAN DEFAULT FALSE // } // Include the matching rule oid, if specified if extensibleMatchingRule.Len() > 0 { packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule.String(), MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule])) } // Include the attribute, if specified if attribute.Len() > 0 { packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute.String(), MatchingRuleAssertionMap[MatchingRuleAssertionType])) } // Add the value (only required child) encodedString, encodeErr := decodeEscapedSymbols(condition.Bytes()) if encodeErr != nil { return packet, newPos, encodeErr } packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchValue, encodedString, MatchingRuleAssertionMap[MatchingRuleAssertionMatchValue])) // Defaults to false, so only include in the sequence if true if extensibleDNAttributes { packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionDNAttributes, extensibleDNAttributes, MatchingRuleAssertionMap[MatchingRuleAssertionDNAttributes])) } case packet.Tag == FilterEqualityMatch && bytes.Equal(condition.Bytes(), _SymbolAny): packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute.String(), FilterMap[FilterPresent]) case packet.Tag == FilterEqualityMatch && bytes.Contains(condition.Bytes(), _SymbolAny): packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute.String(), "Attribute")) packet.Tag = FilterSubstrings packet.Description = FilterMap[uint64(packet.Tag)] seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") parts := bytes.Split(condition.Bytes(), _SymbolAny) for i, part := range parts { if len(part) == 0 { continue } var tag ber.Tag switch i { case 0: tag = FilterSubstringsInitial case len(parts) - 1: tag = FilterSubstringsFinal default: tag = FilterSubstringsAny } encodedString, encodeErr := decodeEscapedSymbols(part) if encodeErr != nil { return packet, newPos, encodeErr } seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)])) } packet.AppendChild(seq) default: encodedString, encodeErr := decodeEscapedSymbols(condition.Bytes()) if encodeErr != nil { return packet, newPos, encodeErr } packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute.String(), "Attribute")) packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition")) } newPos += currentWidth return packet, newPos, err } } // Convert from "ABC\xx\xx\xx" form to literal bytes for transport func decodeEscapedSymbols(src []byte) (string, error) { var ( buffer bytes.Buffer offset int reader = bytes.NewReader(src) byteHex []byte byteVal []byte ) for { runeVal, runeSize, err := reader.ReadRune() if err == io.EOF { return buffer.String(), nil } else if err != nil { return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: failed to read filter: %v", err)) } else if runeVal == unicode.ReplacementChar { return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", offset)) } if runeVal == '\\' { // http://tools.ietf.org/search/rfc4515 // \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not // being a member of UTF1SUBSET. if byteHex == nil { byteHex = make([]byte, 2) byteVal = make([]byte, 1) } if _, err := io.ReadFull(reader, byteHex); err != nil { if err == io.ErrUnexpectedEOF { return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter")) } return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: invalid characters for escape in filter: %v", err)) } if _, err := hexpac.Decode(byteVal, byteHex); err != nil { return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: invalid characters for escape in filter: %v", err)) } buffer.Write(byteVal) } else { buffer.WriteRune(runeVal) } offset += runeSize } }
package ldap import ( "fmt" "io/ioutil" "log" "os" "strings" ber "github.com/go-asn1-ber/asn1-ber" ) // LDAP Application Codes const ( ApplicationBindRequest = 0 ApplicationBindResponse = 1 ApplicationUnbindRequest = 2 ApplicationSearchRequest = 3 ApplicationSearchResultEntry = 4 ApplicationSearchResultDone = 5 ApplicationModifyRequest = 6 ApplicationModifyResponse = 7 ApplicationAddRequest = 8 ApplicationAddResponse = 9 ApplicationDelRequest = 10 ApplicationDelResponse = 11 ApplicationModifyDNRequest = 12 ApplicationModifyDNResponse = 13 ApplicationCompareRequest = 14 ApplicationCompareResponse = 15 ApplicationAbandonRequest = 16 ApplicationSearchResultReference = 19 ApplicationExtendedRequest = 23 ApplicationExtendedResponse = 24 ApplicationIntermediateResponse = 25 ) // ApplicationMap contains human readable descriptions of LDAP Application Codes var ApplicationMap = map[uint8]string{ ApplicationBindRequest: "Bind Request", ApplicationBindResponse: "Bind Response", ApplicationUnbindRequest: "Unbind Request", ApplicationSearchRequest: "Search Request", ApplicationSearchResultEntry: "Search Result Entry", ApplicationSearchResultDone: "Search Result Done", ApplicationModifyRequest: "Modify Request", ApplicationModifyResponse: "Modify Response", ApplicationAddRequest: "Add Request", ApplicationAddResponse: "Add Response", ApplicationDelRequest: "Del Request", ApplicationDelResponse: "Del Response", ApplicationModifyDNRequest: "Modify DN Request", ApplicationModifyDNResponse: "Modify DN Response", ApplicationCompareRequest: "Compare Request", ApplicationCompareResponse: "Compare Response", ApplicationAbandonRequest: "Abandon Request", ApplicationSearchResultReference: "Search Result Reference", ApplicationExtendedRequest: "Extended Request", ApplicationExtendedResponse: "Extended Response", ApplicationIntermediateResponse: "Intermediate Response", } // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) const ( BeheraPasswordExpired = 0 BeheraAccountLocked = 1 BeheraChangeAfterReset = 2 BeheraPasswordModNotAllowed = 3 BeheraMustSupplyOldPassword = 4 BeheraInsufficientPasswordQuality = 5 BeheraPasswordTooShort = 6 BeheraPasswordTooYoung = 7 BeheraPasswordInHistory = 8 ) // BeheraPasswordPolicyErrorMap contains human readable descriptions of Behera Password Policy error codes var BeheraPasswordPolicyErrorMap = map[int8]string{ BeheraPasswordExpired: "Password expired", BeheraAccountLocked: "Account locked", BeheraChangeAfterReset: "Password must be changed", BeheraPasswordModNotAllowed: "Policy prevents password modification", BeheraMustSupplyOldPassword: "Policy requires old password in order to change password", BeheraInsufficientPasswordQuality: "Password fails quality checks", BeheraPasswordTooShort: "Password is too short for policy", BeheraPasswordTooYoung: "Password has been changed too recently", BeheraPasswordInHistory: "New password is in list of old passwords", } var logger = log.New(os.Stderr, "", log.LstdFlags) // Logger allows clients to override the default logger func Logger(l *log.Logger) { logger = l } // Adds descriptions to an LDAP Response packet for debugging func addLDAPDescriptions(packet *ber.Packet) (err error) { defer func() { if r := recover(); r != nil { err = NewError(ErrorDebugging, fmt.Errorf("ldap: cannot process packet to add descriptions: %s", r)) } }() packet.Description = "LDAP Response" packet.Children[0].Description = "Message ID" application := uint8(packet.Children[1].Tag) packet.Children[1].Description = ApplicationMap[application] switch application { case ApplicationBindRequest: err = addRequestDescriptions(packet) case ApplicationBindResponse: err = addDefaultLDAPResponseDescriptions(packet) case ApplicationUnbindRequest: err = addRequestDescriptions(packet) case ApplicationSearchRequest: err = addRequestDescriptions(packet) case ApplicationSearchResultEntry: packet.Children[1].Children[0].Description = "Object Name" packet.Children[1].Children[1].Description = "Attributes" for _, child := range packet.Children[1].Children[1].Children { child.Description = "Attribute" child.Children[0].Description = "Attribute Name" child.Children[1].Description = "Attribute Values" for _, grandchild := range child.Children[1].Children { grandchild.Description = "Attribute Value" } } if len(packet.Children) == 3 { err = addControlDescriptions(packet.Children[2]) } case ApplicationSearchResultDone: err = addDefaultLDAPResponseDescriptions(packet) case ApplicationModifyRequest: err = addRequestDescriptions(packet) case ApplicationModifyResponse: case ApplicationAddRequest: err = addRequestDescriptions(packet) case ApplicationAddResponse: case ApplicationDelRequest: err = addRequestDescriptions(packet) case ApplicationDelResponse: case ApplicationModifyDNRequest: err = addRequestDescriptions(packet) case ApplicationModifyDNResponse: case ApplicationCompareRequest: err = addRequestDescriptions(packet) case ApplicationCompareResponse: case ApplicationAbandonRequest: err = addRequestDescriptions(packet) case ApplicationSearchResultReference: case ApplicationExtendedRequest: err = addRequestDescriptions(packet) case ApplicationExtendedResponse: } return err } func addControlDescriptions(packet *ber.Packet) error { packet.Description = "Controls" for _, child := range packet.Children { var value *ber.Packet controlType := "" child.Description = "Control" switch len(child.Children) { case 0: // at least one child is required for control type return fmt.Errorf("at least one child is required for control type") case 1: // just type, no criticality or value controlType = child.Children[0].Value.(string) child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" case 2: controlType = child.Children[0].Value.(string) child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" // Children[1] could be criticality or value (both are optional) // duck-type on whether this is a boolean if _, ok := child.Children[1].Value.(bool); ok { child.Children[1].Description = "Criticality" } else { child.Children[1].Description = "Control Value" value = child.Children[1] } case 3: // criticality and value present controlType = child.Children[0].Value.(string) child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" child.Children[1].Description = "Criticality" child.Children[2].Description = "Control Value" value = child.Children[2] default: // more than 3 children is invalid return fmt.Errorf("more than 3 children for control packet found") } if value == nil { continue } switch controlType { case ControlTypePaging: value.Description += " (Paging)" if value.Value != nil { valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) if err != nil { return fmt.Errorf("failed to decode data bytes: %s", err) } value.Data.Truncate(0) value.Value = nil valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes() value.AppendChild(valueChildren) } value.Children[0].Description = "Real Search Control Value" value.Children[0].Children[0].Description = "Paging Size" value.Children[0].Children[1].Description = "Cookie" case ControlTypeBeheraPasswordPolicy: value.Description += " (Password Policy - Behera Draft)" if value.Value != nil { valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) if err != nil { return fmt.Errorf("failed to decode data bytes: %s", err) } value.Data.Truncate(0) value.Value = nil value.AppendChild(valueChildren) } sequence := value.Children[0] for _, child := range sequence.Children { if child.Tag == 0 { // Warning warningPacket := child.Children[0] val, err := ber.ParseInt64(warningPacket.Data.Bytes()) if err != nil { return fmt.Errorf("failed to decode data bytes: %s", err) } if warningPacket.Tag == 0 { // timeBeforeExpiration value.Description += " (TimeBeforeExpiration)" warningPacket.Value = val } else if warningPacket.Tag == 1 { // graceAuthNsRemaining value.Description += " (GraceAuthNsRemaining)" warningPacket.Value = val } } else if child.Tag == 1 { // Error bs := child.Data.Bytes() if len(bs) != 1 || bs[0] > 8 { return fmt.Errorf("failed to decode data bytes: %s", "invalid PasswordPolicyResponse enum value") } val := int8(bs[0]) child.Description = "Error" child.Value = val } } } } return nil } func addRequestDescriptions(packet *ber.Packet) error { packet.Description = "LDAP Request" packet.Children[0].Description = "Message ID" packet.Children[1].Description = ApplicationMap[uint8(packet.Children[1].Tag)] if len(packet.Children) == 3 { return addControlDescriptions(packet.Children[2]) } return nil } func addDefaultLDAPResponseDescriptions(packet *ber.Packet) error { resultCode := uint16(LDAPResultSuccess) matchedDN := "" description := "Success" if err := GetLDAPError(packet); err != nil { resultCode = err.(*Error).ResultCode matchedDN = err.(*Error).MatchedDN description = "Error Message" } packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")" packet.Children[1].Children[1].Description = "Matched DN (" + matchedDN + ")" packet.Children[1].Children[2].Description = description if len(packet.Children[1].Children) > 3 { packet.Children[1].Children[3].Description = "Referral" } if len(packet.Children) == 3 { return addControlDescriptions(packet.Children[2]) } return nil } // DebugBinaryFile reads and prints packets from the given filename func DebugBinaryFile(fileName string) error { file, err := ioutil.ReadFile(fileName) if err != nil { return NewError(ErrorDebugging, err) } ber.PrintBytes(os.Stdout, file, "") packet, err := ber.DecodePacketErr(file) if err != nil { return fmt.Errorf("failed to decode packet: %s", err) } if err := addLDAPDescriptions(packet); err != nil { return err } ber.PrintPacket(packet) return nil } func mustEscape(c byte) bool { return c > 0x7f || c == '(' || c == ')' || c == '\\' || c == '*' || c == 0 } // EscapeFilter escapes from the provided LDAP filter string the special // characters in the set `()*\` and those out of the range 0 < c < 0x80, // as defined in RFC4515. func EscapeFilter(filter string) string { const hexValues = "0123456789abcdef" escape := 0 for i := 0; i < len(filter); i++ { if mustEscape(filter[i]) { escape++ } } if escape == 0 { return filter } buf := make([]byte, len(filter)+escape*2) for i, j := 0, 0; i < len(filter); i++ { c := filter[i] if mustEscape(c) { buf[j+0] = '\\' buf[j+1] = hexValues[c>>4] buf[j+2] = hexValues[c&0xf] j += 3 } else { buf[j] = c j++ } } return string(buf) } // EscapeDN escapes distinguished names as described in RFC4514. Characters in the // set `"+,;<>\` are escaped by prepending a backslash, which is also done for trailing // spaces or a leading `#`. Null bytes are replaced with `\00`. func EscapeDN(dn string) string { if dn == "" { return "" } builder := strings.Builder{} for i, r := range dn { // Escape leading and trailing spaces if (i == 0 || i == len(dn)-1) && r == ' ' { builder.WriteRune('\\') builder.WriteRune(r) continue } // Escape leading '#' if i == 0 && r == '#' { builder.WriteRune('\\') builder.WriteRune(r) continue } // Escape characters as defined in RFC4514 switch r { case '"', '+', ',', ';', '<', '>', '\\': builder.WriteRune('\\') builder.WriteRune(r) case '\x00': // Null byte may not be escaped by a leading backslash builder.WriteString("\\00") default: builder.WriteRune(r) } } return builder.String() }
package ldap import ( "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) // ModifyDNRequest holds the request to modify a DN type ModifyDNRequest struct { DN string NewRDN string DeleteOldRDN bool NewSuperior string // Controls hold optional controls to send with the request Controls []Control } // NewModifyDNRequest creates a new request which can be passed to ModifyDN(). // // To move an object in the tree, set the "newSup" to the new parent entry DN. Use an // empty string for just changing the object's RDN. // // For moving the object without renaming, the "rdn" must be the first // RDN of the given DN. // // A call like // // mdnReq := NewModifyDNRequest("uid=someone,dc=example,dc=org", "uid=newname", true, "") // // will setup the request to just rename uid=someone,dc=example,dc=org to // uid=newname,dc=example,dc=org. func NewModifyDNRequest(dn string, rdn string, delOld bool, newSup string) *ModifyDNRequest { return &ModifyDNRequest{ DN: dn, NewRDN: rdn, DeleteOldRDN: delOld, NewSuperior: newSup, } } // NewModifyDNWithControlsRequest creates a new request which can be passed to ModifyDN() // and also allows setting LDAP request controls. // // Refer NewModifyDNRequest for other parameters func NewModifyDNWithControlsRequest(dn string, rdn string, delOld bool, newSup string, controls []Control) *ModifyDNRequest { return &ModifyDNRequest{ DN: dn, NewRDN: rdn, DeleteOldRDN: delOld, NewSuperior: newSup, Controls: controls, } } func (req *ModifyDNRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyDNRequest, nil, "Modify DN Request") pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.NewRDN, "New RDN")) if req.DeleteOldRDN { buf := []byte{0xff} pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, string(buf), "Delete old RDN")) } else { pkt.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, req.DeleteOldRDN, "Delete old RDN")) } if req.NewSuperior != "" { pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.NewSuperior, "New Superior")) } envelope.AppendChild(pkt) if len(req.Controls) > 0 { envelope.AppendChild(encodeControls(req.Controls)) } return nil } // ModifyDN renames the given DN and optionally move to another base (when the "newSup" argument // to NewModifyDNRequest() is not ""). func (l *Conn) ModifyDN(m *ModifyDNRequest) error { msgCtx, err := l.doRequest(m) if err != nil { return err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return err } if packet.Children[1].Tag == ApplicationModifyDNResponse { err := GetLDAPError(packet) if err != nil { return err } } else { return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) } return nil }
package ldap import ( "errors" "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) // Change operation choices const ( AddAttribute = 0 DeleteAttribute = 1 ReplaceAttribute = 2 IncrementAttribute = 3 // (https://tools.ietf.org/html/rfc4525) ) // PartialAttribute for a ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 type PartialAttribute struct { // Type is the type of the partial attribute Type string // Vals are the values of the partial attribute Vals []string } func (p *PartialAttribute) encode() *ber.Packet { seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PartialAttribute") seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, p.Type, "Type")) set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") for _, value := range p.Vals { set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) } seq.AppendChild(set) return seq } // Change for a ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 type Change struct { // Operation is the type of change to be made Operation uint // Modification is the attribute to be modified Modification PartialAttribute } func (c *Change) encode() *ber.Packet { change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(c.Operation), "Operation")) change.AppendChild(c.Modification.encode()) return change } // ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 type ModifyRequest struct { // DN is the distinguishedName of the directory entry to modify DN string // Changes contain the attributes to modify Changes []Change // Controls hold optional controls to send with the request Controls []Control } // Add appends the given attribute to the list of changes to be made func (req *ModifyRequest) Add(attrType string, attrVals []string) { req.appendChange(AddAttribute, attrType, attrVals) } // Delete appends the given attribute to the list of changes to be made func (req *ModifyRequest) Delete(attrType string, attrVals []string) { req.appendChange(DeleteAttribute, attrType, attrVals) } // Replace appends the given attribute to the list of changes to be made func (req *ModifyRequest) Replace(attrType string, attrVals []string) { req.appendChange(ReplaceAttribute, attrType, attrVals) } // Increment appends the given attribute to the list of changes to be made func (req *ModifyRequest) Increment(attrType string, attrVal string) { req.appendChange(IncrementAttribute, attrType, []string{attrVal}) } func (req *ModifyRequest) appendChange(operation uint, attrType string, attrVals []string) { req.Changes = append(req.Changes, Change{operation, PartialAttribute{Type: attrType, Vals: attrVals}}) } func (req *ModifyRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyRequest, nil, "Modify Request") pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) changes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Changes") for _, change := range req.Changes { changes.AppendChild(change.encode()) } pkt.AppendChild(changes) envelope.AppendChild(pkt) if len(req.Controls) > 0 { envelope.AppendChild(encodeControls(req.Controls)) } return nil } // NewModifyRequest creates a modify request for the given DN func NewModifyRequest(dn string, controls []Control) *ModifyRequest { return &ModifyRequest{ DN: dn, Controls: controls, } } // Modify performs the ModifyRequest func (l *Conn) Modify(modifyRequest *ModifyRequest) error { msgCtx, err := l.doRequest(modifyRequest) if err != nil { return err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return err } if packet.Children[1].Tag == ApplicationModifyResponse { err := GetLDAPError(packet) if err != nil { return err } } else { return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) } return nil } // ModifyResult holds the server's response to a modify request type ModifyResult struct { // Controls are the returned controls Controls []Control // Referral is the returned referral Referral string } // ModifyWithResult performs the ModifyRequest and returns the result func (l *Conn) ModifyWithResult(modifyRequest *ModifyRequest) (*ModifyResult, error) { msgCtx, err := l.doRequest(modifyRequest) if err != nil { return nil, err } defer l.finishMessage(msgCtx) result := &ModifyResult{ Controls: make([]Control, 0), } l.Debug.Printf("%d: waiting for response", msgCtx.id) packet, err := l.readPacket(msgCtx) if err != nil { return nil, err } switch packet.Children[1].Tag { case ApplicationModifyResponse: if err = GetLDAPError(packet); err != nil { result.Referral = getReferral(err, packet) return result, err } if len(packet.Children) == 3 { for _, child := range packet.Children[2].Children { decodedChild, err := DecodeControl(child) if err != nil { return nil, errors.New("failed to decode child control: " + err.Error()) } result.Controls = append(result.Controls, decodedChild) } } } l.Debug.Printf("%d: returning", msgCtx.id) return result, nil }
package ldap import ( "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) const ( passwordModifyOID = "1.3.6.1.4.1.4203.1.11.1" ) // PasswordModifyRequest implements the Password Modify Extended Operation as defined in https://www.ietf.org/rfc/rfc3062.txt type PasswordModifyRequest struct { // UserIdentity is an optional string representation of the user associated with the request. // This string may or may not be an LDAPDN [RFC2253]. // If no UserIdentity field is present, the request acts up upon the password of the user currently associated with the LDAP session UserIdentity string // OldPassword, if present, contains the user's current password OldPassword string // NewPassword, if present, contains the desired password for this user NewPassword string } // PasswordModifyResult holds the server response to a PasswordModifyRequest type PasswordModifyResult struct { // GeneratedPassword holds a password generated by the server, if present GeneratedPassword string // Referral are the returned referral Referral string } func (req *PasswordModifyRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Password Modify Extended Operation") pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, passwordModifyOID, "Extended Request Name: Password Modify OID")) extendedRequestValue := ber.Encode(ber.ClassContext, ber.TypePrimitive, 1, nil, "Extended Request Value: Password Modify Request") passwordModifyRequestValue := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Password Modify Request") if req.UserIdentity != "" { passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.UserIdentity, "User Identity")) } if req.OldPassword != "" { passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 1, req.OldPassword, "Old Password")) } if req.NewPassword != "" { passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 2, req.NewPassword, "New Password")) } extendedRequestValue.AppendChild(passwordModifyRequestValue) pkt.AppendChild(extendedRequestValue) envelope.AppendChild(pkt) return nil } // NewPasswordModifyRequest creates a new PasswordModifyRequest // // According to the RFC 3602 (https://tools.ietf.org/html/rfc3062): // userIdentity is a string representing the user associated with the request. // This string may or may not be an LDAPDN (RFC 2253). // If userIdentity is empty then the operation will act on the user associated // with the session. // // oldPassword is the current user's password, it can be empty or it can be // needed depending on the session user access rights (usually an administrator // can change a user's password without knowing the current one) and the // password policy (see pwdSafeModify password policy's attribute) // // newPassword is the desired user's password. If empty the server can return // an error or generate a new password that will be available in the // PasswordModifyResult.GeneratedPassword func NewPasswordModifyRequest(userIdentity string, oldPassword string, newPassword string) *PasswordModifyRequest { return &PasswordModifyRequest{ UserIdentity: userIdentity, OldPassword: oldPassword, NewPassword: newPassword, } } // PasswordModify performs the modification request func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error) { msgCtx, err := l.doRequest(passwordModifyRequest) if err != nil { return nil, err } defer l.finishMessage(msgCtx) packet, err := l.readPacket(msgCtx) if err != nil { return nil, err } result := &PasswordModifyResult{} if packet.Children[1].Tag == ApplicationExtendedResponse { if err = GetLDAPError(packet); err != nil { result.Referral = getReferral(err, packet) return result, err } } else { return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("unexpected Response: %d", packet.Children[1].Tag)) } extendedResponse := packet.Children[1] for _, child := range extendedResponse.Children { if child.Tag == ber.TagEmbeddedPDV { passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes()) if len(passwordModifyResponseValue.Children) == 1 { if passwordModifyResponseValue.Children[0].Tag == ber.TagEOC { result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes()) } } } } return result, nil }
package ldap import ( "errors" ber "github.com/go-asn1-ber/asn1-ber" ) var ( errRespChanClosed = errors.New("ldap: response channel closed") errCouldNotRetMsg = errors.New("ldap: could not retrieve message") // ErrNilConnection is returned if doRequest is called with a nil connection. ErrNilConnection = errors.New("ldap: conn is nil, expected net.Conn") ) type request interface { appendTo(*ber.Packet) error } type requestFunc func(*ber.Packet) error func (f requestFunc) appendTo(p *ber.Packet) error { return f(p) } func (l *Conn) doRequest(req request) (*messageContext, error) { if l == nil || l.conn == nil { return nil, ErrNilConnection } packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) if err := req.appendTo(packet); err != nil { return nil, err } if l.Debug { l.Debug.PrintPacket(packet) } msgCtx, err := l.sendMessage(packet) if err != nil { return nil, err } l.Debug.Printf("%d: returning", msgCtx.id) return msgCtx, nil } func (l *Conn) readPacket(msgCtx *messageContext) (*ber.Packet, error) { l.Debug.Printf("%d: waiting for response", msgCtx.id) packetResponse, ok := <-msgCtx.responses if !ok { return nil, NewError(ErrorNetwork, errRespChanClosed) } packet, err := packetResponse.ReadPacket() l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if err != nil { return nil, err } if packet == nil { return nil, NewError(ErrorNetwork, errCouldNotRetMsg) } if l.Debug { if err = addLDAPDescriptions(packet); err != nil { return nil, err } l.Debug.PrintPacket(packet) } return packet, nil } func getReferral(err error, packet *ber.Packet) (referral string) { if !IsErrorWithCode(err, LDAPResultReferral) { return "" } if len(packet.Children) < 2 { return "" } // The packet Tag itself (of child 2) is generally a ber.TagObjectDescriptor with referrals however OpenLDAP // seemingly returns a ber.Tag.GeneralizedTime. Every currently tested LDAP server which returns referrals returns // an ASN.1 BER packet with the Type of ber.TypeConstructed and Class of ber.ClassApplication however. Thus this // check expressly checks these fields instead. // // Related Issues: // - https://github.com/authelia/authelia/issues/4199 (downstream) if len(packet.Children[1].Children) == 0 || (packet.Children[1].TagType != ber.TypeConstructed || packet.Children[1].ClassType != ber.ClassApplication) { return "" } var ok bool for _, child := range packet.Children[1].Children { // The referral URI itself should be contained within a child which has a Tag of ber.BitString or // ber.TagPrintableString, and the Type of ber.TypeConstructed and the Class of ClassContext. As soon as any of // these conditions is not true we can skip this child. if (child.Tag != ber.TagBitString && child.Tag != ber.TagPrintableString) || child.TagType != ber.TypeConstructed || child.ClassType != ber.ClassContext { continue } if referral, ok = child.Children[0].Value.(string); ok { return referral } } return "" }
package ldap import ( "context" "errors" "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) // Response defines an interface to get data from an LDAP server type Response interface { Entry() *Entry Referral() string Controls() []Control Err() error Next() bool } type searchResponse struct { conn *Conn ch chan *SearchSingleResult entry *Entry referral string controls []Control err error } // Entry returns an entry from the given search request func (r *searchResponse) Entry() *Entry { return r.entry } // Referral returns a referral from the given search request func (r *searchResponse) Referral() string { return r.referral } // Controls returns controls from the given search request func (r *searchResponse) Controls() []Control { return r.controls } // Err returns an error when the given search request was failed func (r *searchResponse) Err() error { return r.err } // Next returns whether next data exist or not func (r *searchResponse) Next() bool { res, ok := <-r.ch if !ok { return false } if res == nil { return false } r.err = res.Error if r.err != nil { return false } r.entry = res.Entry r.referral = res.Referral r.controls = res.Controls return true } func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest) { go func() { defer func() { close(r.ch) if err := recover(); err != nil { r.conn.err = fmt.Errorf("ldap: recovered panic in searchResponse: %v", err) } }() if r.conn.IsClosing() { return } packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, r.conn.nextMessageID(), "MessageID")) // encode search request err := searchRequest.appendTo(packet) if err != nil { r.ch <- &SearchSingleResult{Error: err} return } r.conn.Debug.PrintPacket(packet) msgCtx, err := r.conn.sendMessage(packet) if err != nil { r.ch <- &SearchSingleResult{Error: err} return } defer r.conn.finishMessage(msgCtx) foundSearchSingleResultDone := false for !foundSearchSingleResultDone { r.conn.Debug.Printf("%d: waiting for response", msgCtx.id) select { case <-ctx.Done(): r.conn.Debug.Printf("%d: %s", msgCtx.id, ctx.Err().Error()) return case packetResponse, ok := <-msgCtx.responses: if !ok { err := NewError(ErrorNetwork, errors.New("ldap: response channel closed")) r.ch <- &SearchSingleResult{Error: err} return } packet, err = packetResponse.ReadPacket() r.conn.Debug.Printf("%d: got response %p", msgCtx.id, packet) if err != nil { r.ch <- &SearchSingleResult{Error: err} return } if r.conn.Debug { if err := addLDAPDescriptions(packet); err != nil { r.ch <- &SearchSingleResult{Error: err} return } ber.PrintPacket(packet) } switch packet.Children[1].Tag { case ApplicationSearchResultEntry: result := &SearchSingleResult{ Entry: &Entry{ DN: packet.Children[1].Children[0].Value.(string), Attributes: unpackAttributes(packet.Children[1].Children[1].Children), }, } if len(packet.Children) != 3 { r.ch <- result continue } decoded, err := DecodeControl(packet.Children[2].Children[0]) if err != nil { werr := fmt.Errorf("failed to decode search result entry: %w", err) result.Error = werr r.ch <- result return } result.Controls = append(result.Controls, decoded) r.ch <- result case ApplicationSearchResultDone: if err := GetLDAPError(packet); err != nil { r.ch <- &SearchSingleResult{Error: err} return } if len(packet.Children) == 3 { result := &SearchSingleResult{} for _, child := range packet.Children[2].Children { decodedChild, err := DecodeControl(child) if err != nil { werr := fmt.Errorf("failed to decode child control: %w", err) r.ch <- &SearchSingleResult{Error: werr} return } result.Controls = append(result.Controls, decodedChild) } r.ch <- result } foundSearchSingleResultDone = true case ApplicationSearchResultReference: ref := packet.Children[1].Children[0].Value.(string) r.ch <- &SearchSingleResult{Referral: ref} case ApplicationIntermediateResponse: decoded, err := DecodeControl(packet.Children[1]) if err != nil { werr := fmt.Errorf("failed to decode intermediate response: %w", err) r.ch <- &SearchSingleResult{Error: werr} return } result := &SearchSingleResult{} result.Controls = append(result.Controls, decoded) r.ch <- result default: err := fmt.Errorf("unknown tag: %d", packet.Children[1].Tag) r.ch <- &SearchSingleResult{Error: err} return } } } r.conn.Debug.Printf("%d: returning", msgCtx.id) }() } func newSearchResponse(conn *Conn, bufferSize int) *searchResponse { var ch chan *SearchSingleResult if bufferSize > 0 { ch = make(chan *SearchSingleResult, bufferSize) } else { ch = make(chan *SearchSingleResult) } return &searchResponse{ conn: conn, ch: ch, } }
package ldap import ( "context" "errors" "fmt" "reflect" "sort" "strconv" "strings" "time" ber "github.com/go-asn1-ber/asn1-ber" ) // scope choices const ( ScopeBaseObject = 0 ScopeSingleLevel = 1 ScopeWholeSubtree = 2 // ScopeChildren is an OpenLDAP extension that may not be supported by another directory server. // See: https://github.com/openldap/openldap/blob/7c55484ee153047efd0e562fc1638c1a2525f320/include/ldap.h#L598 ScopeChildren = 3 ) // ScopeMap contains human readable descriptions of scope choices var ScopeMap = map[int]string{ ScopeBaseObject: "Base Object", ScopeSingleLevel: "Single Level", ScopeWholeSubtree: "Whole Subtree", ScopeChildren: "Children", } // derefAliases const ( NeverDerefAliases = 0 DerefInSearching = 1 DerefFindingBaseObj = 2 DerefAlways = 3 ) // DerefMap contains human readable descriptions of derefAliases choices var DerefMap = map[int]string{ NeverDerefAliases: "NeverDerefAliases", DerefInSearching: "DerefInSearching", DerefFindingBaseObj: "DerefFindingBaseObj", DerefAlways: "DerefAlways", } // ErrSizeLimitExceeded will be returned if the search result is exceeding the defined SizeLimit // and enforcing the requested limit is enabled in the search request (EnforceSizeLimit) var ErrSizeLimitExceeded = NewError(ErrorNetwork, errors.New("ldap: size limit exceeded")) // NewEntry returns an Entry object with the specified distinguished name and attribute key-value pairs. // The map of attributes is accessed in alphabetical order of the keys in order to ensure that, for the // same input map of attributes, the output entry will contain the same order of attributes func NewEntry(dn string, attributes map[string][]string) *Entry { var attributeNames []string for attributeName := range attributes { attributeNames = append(attributeNames, attributeName) } sort.Strings(attributeNames) var encodedAttributes []*EntryAttribute for _, attributeName := range attributeNames { encodedAttributes = append(encodedAttributes, NewEntryAttribute(attributeName, attributes[attributeName])) } return &Entry{ DN: dn, Attributes: encodedAttributes, } } // Entry represents a single search result entry type Entry struct { // DN is the distinguished name of the entry DN string // Attributes are the returned attributes for the entry Attributes []*EntryAttribute } // GetAttributeValues returns the values for the named attribute, or an empty list func (e *Entry) GetAttributeValues(attribute string) []string { for _, attr := range e.Attributes { if attr.Name == attribute { return attr.Values } } return []string{} } // GetEqualFoldAttributeValues returns the values for the named attribute, or an // empty list. Attribute matching is done with strings.EqualFold. func (e *Entry) GetEqualFoldAttributeValues(attribute string) []string { for _, attr := range e.Attributes { if strings.EqualFold(attribute, attr.Name) { return attr.Values } } return []string{} } // GetRawAttributeValues returns the byte values for the named attribute, or an empty list func (e *Entry) GetRawAttributeValues(attribute string) [][]byte { for _, attr := range e.Attributes { if attr.Name == attribute { return attr.ByteValues } } return [][]byte{} } // GetEqualFoldRawAttributeValues returns the byte values for the named attribute, or an empty list func (e *Entry) GetEqualFoldRawAttributeValues(attribute string) [][]byte { for _, attr := range e.Attributes { if strings.EqualFold(attr.Name, attribute) { return attr.ByteValues } } return [][]byte{} } // GetAttributeValue returns the first value for the named attribute, or "" func (e *Entry) GetAttributeValue(attribute string) string { values := e.GetAttributeValues(attribute) if len(values) == 0 { return "" } return values[0] } // GetEqualFoldAttributeValue returns the first value for the named attribute, or "". // Attribute comparison is done with strings.EqualFold. func (e *Entry) GetEqualFoldAttributeValue(attribute string) string { values := e.GetEqualFoldAttributeValues(attribute) if len(values) == 0 { return "" } return values[0] } // GetRawAttributeValue returns the first value for the named attribute, or an empty slice func (e *Entry) GetRawAttributeValue(attribute string) []byte { values := e.GetRawAttributeValues(attribute) if len(values) == 0 { return []byte{} } return values[0] } // GetEqualFoldRawAttributeValue returns the first value for the named attribute, or an empty slice func (e *Entry) GetEqualFoldRawAttributeValue(attribute string) []byte { values := e.GetEqualFoldRawAttributeValues(attribute) if len(values) == 0 { return []byte{} } return values[0] } // Print outputs a human-readable description func (e *Entry) Print() { fmt.Printf("DN: %s\n", e.DN) for _, attr := range e.Attributes { attr.Print() } } // PrettyPrint outputs a human-readable description indenting func (e *Entry) PrettyPrint(indent int) { fmt.Printf("%sDN: %s\n", strings.Repeat(" ", indent), e.DN) for _, attr := range e.Attributes { attr.PrettyPrint(indent + 2) } } // Describe the tag to use for struct field tags const decoderTagName = "ldap" // readTag will read the reflect.StructField value for // the key defined in decoderTagName. If omitempty is // specified, the field may not be filled. func readTag(f reflect.StructField) (string, bool) { val, ok := f.Tag.Lookup(decoderTagName) if !ok { return f.Name, false } opts := strings.Split(val, ",") omit := false if len(opts) == 2 { omit = opts[1] == "omitempty" } return opts[0], omit } // Unmarshal parses the Entry in the value pointed to by i // // Currently, this methods only supports struct fields of type // string, *string, []string, int, int64, []byte, *DN, []*DN or time.Time. // Other field types will not be regarded. If the field type is a string or int but multiple // attribute values are returned, the first value will be used to fill the field. // // Example: // // type UserEntry struct { // // Fields with the tag key `dn` are automatically filled with the // // objects distinguishedName. This can be used multiple times. // DN string `ldap:"dn"` // // // This field will be filled with the attribute value for // // userPrincipalName. An attribute can be read into a struct field // // multiple times. Missing attributes will not result in an error. // UserPrincipalName string `ldap:"userPrincipalName"` // // // memberOf may have multiple values. If you don't // // know the amount of attribute values at runtime, use a string array. // MemberOf []string `ldap:"memberOf"` // // // ID is an integer value, it will fail unmarshaling when the given // // attribute value cannot be parsed into an integer. // ID int `ldap:"id"` // // // LongID is similar to ID but uses an int64 instead. // LongID int64 `ldap:"longId"` // // // Data is similar to MemberOf a slice containing all attribute // // values. // Data []byte `ldap:"data"` // // // Time is parsed with the generalizedTime spec into a time.Time // Created time.Time `ldap:"createdTimestamp"` // // // *DN is parsed with the ParseDN // Owner *ldap.DN `ldap:"owner"` // // // []*DN is parsed with the ParseDN // Children []*ldap.DN `ldap:"children"` // // // This won't work, as the field is not of type string. For this // // to work, you'll have to temporarily store the result in string // // (or string array) and convert it to the desired type afterwards. // UserAccountControl uint32 `ldap:"userPrincipalName"` // } // user := UserEntry{} // // if err := result.Unmarshal(&user); err != nil { // // ... // } func (e *Entry) Unmarshal(i interface{}) (err error) { // Make sure it's a ptr if vo := reflect.ValueOf(i).Kind(); vo != reflect.Ptr { return fmt.Errorf("ldap: cannot use %s, expected pointer to a struct", vo) } sv, st := reflect.ValueOf(i).Elem(), reflect.TypeOf(i).Elem() // Make sure it's pointing to a struct if sv.Kind() != reflect.Struct { return fmt.Errorf("ldap: expected pointer to a struct, got %s", sv.Kind()) } for n := 0; n < st.NumField(); n++ { // Holds struct field value and type fv, ft := sv.Field(n), st.Field(n) // skip unexported fields if ft.PkgPath != "" { continue } // omitempty can be safely discarded, as it's not needed when unmarshalling fieldTag, _ := readTag(ft) // Fill the field with the distinguishedName if the tag key is `dn` if fieldTag == "dn" { fv.SetString(e.DN) continue } values := e.GetAttributeValues(fieldTag) if len(values) == 0 { continue } switch fv.Interface().(type) { case []string: for _, item := range values { fv.Set(reflect.Append(fv, reflect.ValueOf(item))) } case string: fv.SetString(values[0]) case *string: fv.Set(reflect.ValueOf(&values[0])) case []byte: fv.SetBytes([]byte(values[0])) case int, int64: intVal, err := strconv.ParseInt(values[0], 10, 64) if err != nil { return fmt.Errorf("ldap: could not parse value '%s' into int field", values[0]) } fv.SetInt(intVal) case time.Time: t, err := ber.ParseGeneralizedTime([]byte(values[0])) if err != nil { return fmt.Errorf("ldap: could not parse value '%s' into time.Time field", values[0]) } fv.Set(reflect.ValueOf(t)) case *DN: dn, err := ParseDN(values[0]) if err != nil { return fmt.Errorf("ldap: could not parse value '%s' into *ldap.DN field", values[0]) } fv.Set(reflect.ValueOf(dn)) case []*DN: for _, item := range values { dn, err := ParseDN(item) if err != nil { return fmt.Errorf("ldap: could not parse value '%s' into *ldap.DN field", item) } fv.Set(reflect.Append(fv, reflect.ValueOf(dn))) } default: return fmt.Errorf("ldap: expected field to be of type string, *string, []string, int, int64, []byte, *DN, []*DN or time.Time, got %v", ft.Type) } } return } // NewEntryAttribute returns a new EntryAttribute with the desired key-value pair func NewEntryAttribute(name string, values []string) *EntryAttribute { var bytes [][]byte for _, value := range values { bytes = append(bytes, []byte(value)) } return &EntryAttribute{ Name: name, Values: values, ByteValues: bytes, } } // EntryAttribute holds a single attribute type EntryAttribute struct { // Name is the name of the attribute Name string // Values contain the string values of the attribute Values []string // ByteValues contain the raw values of the attribute ByteValues [][]byte } // Print outputs a human-readable description func (e *EntryAttribute) Print() { fmt.Printf("%s: %s\n", e.Name, e.Values) } // PrettyPrint outputs a human-readable description with indenting func (e *EntryAttribute) PrettyPrint(indent int) { fmt.Printf("%s%s: %s\n", strings.Repeat(" ", indent), e.Name, e.Values) } // SearchResult holds the server's response to a search request type SearchResult struct { // Entries are the returned entries Entries []*Entry // Referrals are the returned referrals Referrals []string // Controls are the returned controls Controls []Control } // Print outputs a human-readable description func (s *SearchResult) Print() { for _, entry := range s.Entries { entry.Print() } } // PrettyPrint outputs a human-readable description with indenting func (s *SearchResult) PrettyPrint(indent int) { for _, entry := range s.Entries { entry.PrettyPrint(indent) } } // appendTo appends all entries of `s` to `r` func (s *SearchResult) appendTo(r *SearchResult) { r.Entries = append(r.Entries, s.Entries...) r.Referrals = append(r.Referrals, s.Referrals...) r.Controls = append(r.Controls, s.Controls...) } // SearchSingleResult holds the server's single entry response to a search request type SearchSingleResult struct { // Entry is the returned entry Entry *Entry // Referral is the returned referral Referral string // Controls are the returned controls Controls []Control // Error is set when the search request was failed Error error } // Print outputs a human-readable description func (s *SearchSingleResult) Print() { s.Entry.Print() } // PrettyPrint outputs a human-readable description with indenting func (s *SearchSingleResult) PrettyPrint(indent int) { s.Entry.PrettyPrint(indent) } // SearchRequest represents a search request to send to the server type SearchRequest struct { BaseDN string Scope int DerefAliases int SizeLimit int TimeLimit int TypesOnly bool Filter string Attributes []string Controls []Control // EnforceSizeLimit will hard limit the maximum number of entries parsed, in case the directory // server returns more results than requested. This setting is disabled by default and does not // work in async search requests. EnforceSizeLimit bool } func (req *SearchRequest) appendTo(envelope *ber.Packet) error { pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request") pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.BaseDN, "Base DN")) pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(req.Scope), "Scope")) pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(req.DerefAliases), "Deref Aliases")) pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(req.SizeLimit), "Size Limit")) pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(req.TimeLimit), "Time Limit")) pkt.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, req.TypesOnly, "Types Only")) // compile and encode filter filterPacket, err := CompileFilter(req.Filter) if err != nil { return err } pkt.AppendChild(filterPacket) // encode attributes attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") for _, attribute := range req.Attributes { attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) } pkt.AppendChild(attributesPacket) envelope.AppendChild(pkt) if len(req.Controls) > 0 { envelope.AppendChild(encodeControls(req.Controls)) } return nil } // NewSearchRequest creates a new search request func NewSearchRequest( BaseDN string, Scope, DerefAliases, SizeLimit, TimeLimit int, TypesOnly bool, Filter string, Attributes []string, Controls []Control, ) *SearchRequest { return &SearchRequest{ BaseDN: BaseDN, Scope: Scope, DerefAliases: DerefAliases, SizeLimit: SizeLimit, TimeLimit: TimeLimit, TypesOnly: TypesOnly, Filter: Filter, Attributes: Attributes, Controls: Controls, } } // SearchWithPaging accepts a search request and desired page size in order to execute LDAP queries to fulfill the // search request. All paged LDAP query responses will be buffered and the final result will be returned atomically. // The following four cases are possible given the arguments: // - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size // - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries // - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request // - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries // // A requested pagingSize of 0 is interpreted as no limit by LDAP servers. func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) { var pagingControl *ControlPaging control := FindControl(searchRequest.Controls, ControlTypePaging) if control == nil { pagingControl = NewControlPaging(pagingSize) searchRequest.Controls = append(searchRequest.Controls, pagingControl) } else { castControl, ok := control.(*ControlPaging) if !ok { return nil, fmt.Errorf("expected paging control to be of type *ControlPaging, got %v", control) } if castControl.PagingSize != pagingSize { return nil, fmt.Errorf("paging size given in search request (%d) conflicts with size given in search call (%d)", castControl.PagingSize, pagingSize) } pagingControl = castControl } searchResult := new(SearchResult) for { result, err := l.Search(searchRequest) if result != nil { result.appendTo(searchResult) } else { if err == nil { // We have to do this beautifulness in case something absolutely strange happens, which // should only occur in case there is no packet, but also no error. return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) } } if err != nil { // If an error occurred, all results that have been received so far will be returned return searchResult, err } l.Debug.Printf("Looking for Paging Control...") pagingResult := FindControl(result.Controls, ControlTypePaging) if pagingResult == nil { pagingControl = nil l.Debug.Printf("Could not find paging control. Breaking...") break } cookie := pagingResult.(*ControlPaging).Cookie if len(cookie) == 0 { pagingControl = nil l.Debug.Printf("Could not find cookie. Breaking...") break } pagingControl.SetCookie(cookie) } if pagingControl != nil { l.Debug.Printf("Abandoning Paging...") pagingControl.PagingSize = 0 if _, err := l.Search(searchRequest); err != nil { return searchResult, err } } return searchResult, nil } // Search performs the given search request func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { msgCtx, err := l.doRequest(searchRequest) if err != nil { return nil, err } defer l.finishMessage(msgCtx) result := &SearchResult{ Entries: make([]*Entry, 0), Referrals: make([]string, 0), Controls: make([]Control, 0), } for { packet, err := l.readPacket(msgCtx) if err != nil { return result, err } switch packet.Children[1].Tag { case 4: if searchRequest.EnforceSizeLimit && searchRequest.SizeLimit > 0 && len(result.Entries) >= searchRequest.SizeLimit { return result, ErrSizeLimitExceeded } attr := make([]*ber.Packet, 0) if len(packet.Children[1].Children) > 1 { attr = packet.Children[1].Children[1].Children } entry := &Entry{ DN: packet.Children[1].Children[0].Value.(string), Attributes: unpackAttributes(attr), } result.Entries = append(result.Entries, entry) case 5: err := GetLDAPError(packet) if err != nil { return result, err } if len(packet.Children) == 3 { for _, child := range packet.Children[2].Children { decodedChild, err := DecodeControl(child) if err != nil { return result, fmt.Errorf("failed to decode child control: %s", err) } result.Controls = append(result.Controls, decodedChild) } } return result, nil case 19: result.Referrals = append(result.Referrals, packet.Children[1].Children[0].Value.(string)) } } } // SearchAsync performs a search request and returns all search results asynchronously. // This means you get all results until an error happens (or the search successfully finished), // e.g. for size / time limited requests all are recieved until the limit is reached. // To stop the search, call cancel function of the context. func (l *Conn) SearchAsync( ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response { r := newSearchResponse(l, bufferSize) r.start(ctx, searchRequest) return r } // Syncrepl is a short name for LDAP Sync Replication engine that works on the // consumer-side. This can perform a persistent search and returns an entry // when the entry is updated on the server side. // To stop the search, call cancel function of the context. func (l *Conn) Syncrepl( ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte, reloadHint bool, ) Response { control := NewControlSyncRequest(mode, cookie, reloadHint) searchRequest.Controls = append(searchRequest.Controls, control) r := newSearchResponse(l, bufferSize) r.start(ctx, searchRequest) return r } // unpackAttributes will extract all given LDAP attributes and it's values // from the ber.Packet func unpackAttributes(children []*ber.Packet) []*EntryAttribute { entries := make([]*EntryAttribute, len(children)) for i, child := range children { length := len(child.Children[1].Children) entry := &EntryAttribute{ Name: child.Children[0].Value.(string), // pre-allocate the slice since we can determine // the number of attributes at this point Values: make([]string, length), ByteValues: make([][]byte, length), } for i, value := range child.Children[1].Children { entry.ByteValues[i] = value.ByteValue entry.Values[i] = value.Value.(string) } entries[i] = entry } return entries } // DirSync does a Search with dirSync Control. func (l *Conn) DirSync( searchRequest *SearchRequest, flags int64, maxAttrCount int64, cookie []byte, ) (*SearchResult, error) { control := FindControl(searchRequest.Controls, ControlTypeDirSync) if control == nil { c := NewRequestControlDirSync(flags, maxAttrCount, cookie) searchRequest.Controls = append(searchRequest.Controls, c) } else { c := control.(*ControlDirSync) if c.Flags != flags { return nil, fmt.Errorf("flags given in search request (%d) conflicts with flags given in search call (%d)", c.Flags, flags) } if c.MaxAttrCount != maxAttrCount { return nil, fmt.Errorf("MaxAttrCnt given in search request (%d) conflicts with maxAttrCount given in search call (%d)", c.MaxAttrCount, maxAttrCount) } } searchResult, err := l.Search(searchRequest) l.Debug.Printf("Looking for result...") if err != nil { return nil, err } if searchResult == nil { return nil, NewError(ErrorNetwork, errors.New("ldap: packet not received")) } l.Debug.Printf("Looking for DirSync Control...") resultControl := FindControl(searchResult.Controls, ControlTypeDirSync) if resultControl == nil { l.Debug.Printf("Could not find dirSyncControl control. Breaking...") return searchResult, nil } cookie = resultControl.(*ControlDirSync).Cookie if len(cookie) == 0 { l.Debug.Printf("Could not find cookie. Breaking...") return searchResult, nil } return searchResult, nil } // DirSyncDirSyncAsync performs a search request and returns all search results // asynchronously. This is efficient when the server returns lots of entries. func (l *Conn) DirSyncAsync( ctx context.Context, searchRequest *SearchRequest, bufferSize int, flags, maxAttrCount int64, cookie []byte, ) Response { control := NewRequestControlDirSync(flags, maxAttrCount, cookie) searchRequest.Controls = append(searchRequest.Controls, control) r := newSearchResponse(l, bufferSize) r.start(ctx, searchRequest) return r }
package ldap import ( "errors" ber "github.com/go-asn1-ber/asn1-ber" ) // ErrConnUnbound is returned when Unbind is called on an already closing connection. var ErrConnUnbound = NewError(ErrorNetwork, errors.New("ldap: connection is closed")) type unbindRequest struct{} func (unbindRequest) appendTo(envelope *ber.Packet) error { envelope.AppendChild(ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationUnbindRequest, nil, ApplicationMap[ApplicationUnbindRequest])) return nil } // Unbind will perform an unbind request. The Unbind operation // should be thought of as the "quit" operation. // See https://datatracker.ietf.org/doc/html/rfc4511#section-4.3 func (l *Conn) Unbind() error { if l.IsClosing() { return ErrConnUnbound } _, err := l.doRequest(unbindRequest{}) if err != nil { return err } // Sending an unbindRequest will make the connection unusable. // Pending requests will fail with: // LDAP Result Code 200 "Network Error": ldap: response channel closed l.Close() return nil }
package ldap // This file contains the "Who Am I?" extended operation as specified in rfc 4532 // // https://tools.ietf.org/html/rfc4532 import ( "errors" "fmt" ber "github.com/go-asn1-ber/asn1-ber" ) type whoAmIRequest bool // WhoAmIResult is returned by the WhoAmI() call type WhoAmIResult struct { AuthzID string } func (r whoAmIRequest) encode() (*ber.Packet, error) { request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Who Am I? Extended Operation") request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, ControlTypeWhoAmI, "Extended Request Name: Who Am I? OID")) return request, nil } // WhoAmI returns the authzId the server thinks we are, you may pass controls // like a Proxied Authorization control func (l *Conn) WhoAmI(controls []Control) (*WhoAmIResult, error) { packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) req := whoAmIRequest(true) encodedWhoAmIRequest, err := req.encode() if err != nil { return nil, err } packet.AppendChild(encodedWhoAmIRequest) if len(controls) != 0 { packet.AppendChild(encodeControls(controls)) } l.Debug.PrintPacket(packet) msgCtx, err := l.sendMessage(packet) if err != nil { return nil, err } defer l.finishMessage(msgCtx) result := &WhoAmIResult{} l.Debug.Printf("%d: waiting for response", msgCtx.id) packetResponse, ok := <-msgCtx.responses if !ok { return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) } packet, err = packetResponse.ReadPacket() l.Debug.Printf("%d: got response %p", msgCtx.id, packet) if err != nil { return nil, err } if packet == nil { return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) } if l.Debug { if err := addLDAPDescriptions(packet); err != nil { return nil, err } ber.PrintPacket(packet) } if packet.Children[1].Tag == ApplicationExtendedResponse { if err := GetLDAPError(packet); err != nil { return nil, err } } else { return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag)) } extendedResponse := packet.Children[1] for _, child := range extendedResponse.Children { if child.Tag == 11 { result.AuthzID = ber.DecodeString(child.Data.Bytes()) } } return result, nil }