diff --git a/ldap/decode_dn_contrib.go b/ldap/decode_dn_contrib.go new file mode 100644 index 0000000..03933b1 --- /dev/null +++ b/ldap/decode_dn_contrib.go @@ -0,0 +1,111 @@ +// The MIT License (MIT) + +// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) +// Portions copyright (c) 2015-2016 go-ldap Authors + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package ldap + +import ( + "encoding/hex" + "errors" + "fmt" + "strings" + + ldap "github.com/go-ldap/ldap/v3" +) + +// DecodeDN - remove leading and trailing spaces from the attribute type and value +// and unescape any escaped characters in these fields +// +// pulled from the go-ldap library +// https://github.com/go-ldap/ldap/blob/dbdc485259442f987d83e604cd4f5859cfc1be58/dn.go +func DecodeDN(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 "", ldap.NewError(34, 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 "", ldap.NewError(34, errors.New("unable 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 "", ldap.NewError(34, fmt.Errorf("unable to decode escaped character: invalid byte: %s", 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 "", ldap.NewError(34, errors.New("unable to decode escaped character: "+err.Error())) + } else if n != 1 { + return "", ldap.NewError(34, fmt.Errorf("unable 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 +} + +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 += " " + } + + return noSpaces +} diff --git a/ldap/decode_dn_contrib_test.go b/ldap/decode_dn_contrib_test.go new file mode 100644 index 0000000..cc10a2a --- /dev/null +++ b/ldap/decode_dn_contrib_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2015-2024 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ldap + +import ( + "errors" + "fmt" + "testing" +) + +func TestDecodeDN(t *testing.T) { + testCases := []struct { + input string + expected string + err error + }{ + { + input: "cn=foo,dc=example,dc=com", + expected: "cn=foo,dc=example,dc=com", + }, + { + input: `cn=\d0\bf\d1\80\d0\b5\d1\86\d0\b5\d0\b4\d0\b5\d0\bd\d1\82 \d1\82\d0\b5\d1\81\d1\82,dc=example,dc=com`, + expected: "cn=прецедент тест,dc=example,dc=com", + }, + { + input: `cn=pr\c3\bcfen,dc=example,dc=com`, + expected: "cn=prüfen,dc=example,dc=com", + }, + { + input: `cn=fo\20o,dc=example,dc=com`, + expected: "cn=fo o,dc=example,dc=com", + }, + { + input: `cn=\e6\b5\8b\e8\af\95,dc=example,dc=com`, + expected: "cn=测试,dc=example,dc=com", + }, + { + input: `cn=\e6\b8\ac\e8\a9\a6,dc=example,dc=com`, + expected: "cn=測試,dc=example,dc=com", + }, + { + input: `cn=svc\ef\b9\92algorithm,dc=example,dc=com`, + expected: "cn=svc﹒algorithm,dc=example,dc=com", + }, + { + input: `cn=\e0\a4\9c\e0\a4\be\e0\a4\81\e0\a4\9a,dc=example,dc=com`, + expected: "cn=जाँच,dc=example,dc=com", + }, + { + input: `cn=\f0\9f\a7\aa\f0\9f\93\9d,dc=example,dc=com`, + expected: "cn=🧪📝,dc=example,dc=com", + }, + { + input: `cn=foo,dc=example,dc=com\`, + err: fmt.Errorf("got corrupted escaped character: '%s'", `cn=foo,dc=example,dc=com\`), + }, + { + input: `cn=foo,dc=example,dc=com\a`, + err: fmt.Errorf("unable to decode escaped character: encoding/hex: invalid byte: %s", "a"), + }, + } + for i, testCase := range testCases { + t.Run(fmt.Sprintf("test case %d", i), func(t *testing.T) { + output, err := DecodeDN(testCase.input) + if err != nil && testCase.err == nil { + t.Fatalf("unexpected error: %v", err) + } + if testCase.err != nil && errors.Is(err, testCase.err) { + t.Fatalf("expected error `%v`, got `%v`", testCase.err, err) + } + if output != testCase.expected { + t.Fatalf("expected %q, got %q", testCase.expected, output) + } + }) + } +}