Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 45 additions & 38 deletions pkg/utils/ldap/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,24 +541,47 @@ func (i *Identity) GetLDAPGroupMembers(ctx context.Context, lc ldap.Client, grou
return memberEntries, nil
}

func filterEscapeBinaryUUID(value uuid.UUID) string {
filtered := ""
for _, b := range value {
filtered = fmt.Sprintf("%s\\%02x", filtered, b)
func filterEscapeAttribute(attribute string, binary bool, id string) (string, error) {
var escaped string
if binary {
pid, err := uuid.Parse(id)
if err != nil {
err := fmt.Errorf("error parsing id '%s' as UUID: %w", id, err)
return "", err
}
escaped = filterEscapeBinaryUUID(attribute, pid)
} else {
escaped = ldap.EscapeFilter(id)
}
return escaped, nil
}

func filterEscapeBinaryUUID(attribute string, value uuid.UUID) string {
bytes := value[:]

// AD stores objectGUID with mixed endianness 🤪 - swap first 3 components
if strings.EqualFold(attribute, "objectguid") {
bytes = []byte{
value[3], value[2], value[1], value[0], // First component (4 bytes) - reverse
value[5], value[4], // Second component (2 bytes) - reverse
value[7], value[6], // Third component (2 bytes) - reverse
value[8], value[9], value[10], value[11], value[12], value[13], value[14], value[15], // Last 8 bytes - keep as-is
}
}

var filtered strings.Builder
filtered.Grow(len(bytes) * 3) // Pre-allocate: each byte becomes "\xx"
for _, b := range bytes {
fmt.Fprintf(&filtered, "\\%02x", b)
}
return filtered
return filtered.String()
}

func (i *Identity) getUserFilter(uid *identityUser.UserId) (string, error) {
var escapedUUID string
if i.User.Schema.IDIsOctetString {
id, err := uuid.Parse(uid.GetOpaqueId())
if err != nil {
return "", fmt.Errorf("error parsing OpaqueID '%s' as UUID: %w", uid, err)
}
escapedUUID = filterEscapeBinaryUUID(id)
} else {
escapedUUID = ldap.EscapeFilter(uid.GetOpaqueId())
escapedUUID, err := filterEscapeAttribute(i.User.Schema.ID, i.User.Schema.IDIsOctetString, uid.GetOpaqueId())
if err != nil {
return "", fmt.Errorf("error parsing OpaqueID '%s' as UUID: %w", uid, err)
}
return fmt.Sprintf("(&%s(objectclass=%s)%s(%s=%s))",
i.User.Filter,
Expand Down Expand Up @@ -586,14 +609,9 @@ func (i *Identity) getUserAttributeFilter(attribute, value, tenantID string) (st
default:
return "", errors.New("ldap: invalid field " + attribute)
}
if attribute == i.User.Schema.ID && i.User.Schema.IDIsOctetString {
id, err := uuid.Parse(value)
if err != nil {
return "", fmt.Errorf("error parsing OpaqueID '%s' as UUID: %w", value, err)
}
value = filterEscapeBinaryUUID(id)
} else {
value = ldap.EscapeFilter(value)
value, err := filterEscapeAttribute(i.User.Schema.ID, i.User.Schema.IDIsOctetString, value)
if err != nil {
return "", fmt.Errorf("error parsing attribute '%s' value '%s' as UUID: %w", attribute, value, err)
}
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s)%s%s)",
i.User.Filter,
Expand Down Expand Up @@ -719,15 +737,9 @@ func (i *Identity) getGroupMemberFilter(memberName string) string {
}

func (i *Identity) getGroupFilter(id string) (string, error) {
var escapedUUID string
if i.Group.Schema.IDIsOctetString {
id, err := uuid.Parse(id)
if err != nil {
return "", fmt.Errorf("error parsing OpaqueID '%s' as UUID: %w", id, err)
}
escapedUUID = filterEscapeBinaryUUID(id)
} else {
escapedUUID = ldap.EscapeFilter(id)
escapedUUID, err := filterEscapeAttribute(i.Group.Schema.ID, i.Group.Schema.IDIsOctetString, id)
if err != nil {
return "", fmt.Errorf("error parsing attribute '%s' value '%s' as UUID: %w", i.Group.Schema.ID, id, err)
}

return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s))",
Expand All @@ -753,14 +765,9 @@ func (i *Identity) getGroupAttributeFilter(attribute, value string) (string, err
default:
return "", errors.New("ldap: invalid field " + attribute)
}
if attribute == i.Group.Schema.ID && i.Group.Schema.IDIsOctetString {
id, err := uuid.Parse(value)
if err != nil {
return "", fmt.Errorf("error parsing OpaqueID '%s' as UUID: %w", value, err)
}
value = filterEscapeBinaryUUID(id)
} else {
value = ldap.EscapeFilter(value)
value, err := filterEscapeAttribute(i.Group.Schema.ID, i.Group.Schema.IDIsOctetString, value)
if err != nil {
return "", fmt.Errorf("error parsing attribute '%s' value '%s' as UUID: %w", attribute, value, err)
}
return fmt.Sprintf("(&%s(objectclass=%s)(%s=%s))",
i.Group.Filter,
Expand Down
61 changes: 61 additions & 0 deletions pkg/utils/ldap/identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ldap

import (
"testing"

"github.com/google/uuid"
)

func TestFilterEscapeBinaryUUID(t *testing.T) {
tests := []struct {
name string
attribute string
uuidStr string
want string
}{
{
name: "objectGUID with AD mixed endianness",
attribute: "objectGUID",
uuidStr: "d227cf8e-986e-4a05-8860-af193056278a",
want: "\\8e\\cf\\27\\d2\\6e\\98\\05\\4a\\88\\60\\af\\19\\30\\56\\27\\8a",
},
{
name: "objectGUID case insensitive",
attribute: "ObjectGuid",
uuidStr: "d227cf8e-986e-4a05-8860-af193056278a",
want: "\\8e\\cf\\27\\d2\\6e\\98\\05\\4a\\88\\60\\af\\19\\30\\56\\27\\8a",
},
{
name: "objectGUID uppercase",
attribute: "OBJECTGUID",
uuidStr: "d227cf8e-986e-4a05-8860-af193056278a",
want: "\\8e\\cf\\27\\d2\\6e\\98\\05\\4a\\88\\60\\af\\19\\30\\56\\27\\8a",
},
{
name: "other attribute no byte swapping",
attribute: "entryUUID",
uuidStr: "550e8400-e29b-41d4-a716-446655440000",
want: "\\55\\0e\\84\\00\\e2\\9b\\41\\d4\\a7\\16\\44\\66\\55\\44\\00\\00",
},
{
name: "other attribute with different UUID",
attribute: "someOtherId",
uuidStr: "123e4567-e89b-12d3-a456-426614174000",
want: "\\12\\3e\\45\\67\\e8\\9b\\12\\d3\\a4\\56\\42\\66\\14\\17\\40\\00",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
uuidVal, err := uuid.Parse(tt.uuidStr)
if err != nil {
t.Fatalf("failed to parse UUID: %v", err)
}

got := filterEscapeBinaryUUID(tt.attribute, uuidVal)
if got != tt.want {
t.Errorf("filterEscapeBinaryUUID() = %v, want %v", got, tt.want)
}
})
}
}