Merge pull request #1901 from opencloud-eu/handle-objectguid-endianness

handle objectguid endianess
This commit is contained in:
Jörn Friedrich Dreyer
2025-11-25 08:26:12 +01:00
committed by GitHub
3 changed files with 124 additions and 16 deletions

View File

@@ -497,7 +497,7 @@ func (i *LDAP) searchLDAPEntryByFilter(basedn string, attrs []string, filter str
return res.Entries[0], nil
}
func filterEscapeUUID(binary bool, id string) (string, error) {
func filterEscapeAttribute(attribute string, binary bool, id string) (string, error) {
var escaped string
if binary {
pid, err := uuid.Parse(id)
@@ -505,17 +505,44 @@ func filterEscapeUUID(binary bool, id string) (string, error) {
err := fmt.Errorf("error parsing id '%s' as UUID: %w", id, err)
return "", err
}
for _, b := range pid {
escaped = fmt.Sprintf("%s\\%02x", escaped, b)
}
escaped = filterEscapeBinaryUUID(attribute, pid)
} else {
escaped = ldap.EscapeFilter(id)
}
return escaped, nil
}
// swapObjectGUIDBytes converts between AD's mixed-endian objectGUID format and standard UUID byte order
func swapObjectGUIDBytes(value []byte) []byte {
if len(value) != 16 {
return value
}
return []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
}
}
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 = swapObjectGUIDBytes(bytes)
}
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.String()
}
func (i *LDAP) getLDAPUserByID(id string) (*ldap.Entry, error) {
idString, err := filterEscapeUUID(i.userIDisOctetString, id)
idString, err := filterEscapeAttribute(i.userAttributeMap.id, i.userIDisOctetString, id)
if err != nil {
return nil, fmt.Errorf("invalid User id: %w", err)
}
@@ -524,7 +551,7 @@ func (i *LDAP) getLDAPUserByID(id string) (*ldap.Entry, error) {
}
func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) {
idString, err := filterEscapeUUID(i.userIDisOctetString, nameOrID)
idString, err := filterEscapeAttribute(i.userAttributeMap.id, i.userIDisOctetString, nameOrID)
// err != nil just means that this is not an uuid, so we can skip the uuid filter part
// and just filter by name
var filter string
@@ -812,16 +839,25 @@ func (i *LDAP) updateUserPassword(ctx context.Context, dn, password string) erro
return err
}
func (i *LDAP) ldapUUIDtoString(e *ldap.Entry, attrType string, binary bool) (string, error) {
func (i *LDAP) ldapUUIDtoString(e *ldap.Entry, attribute string, binary bool) (string, error) {
if binary {
rawValue := e.GetEqualFoldRawAttributeValue(attrType)
value, err := uuid.FromBytes(rawValue)
if err == nil {
return value.String(), nil
value := e.GetEqualFoldRawAttributeValue(attribute)
if len(value) != 16 {
return "", fmt.Errorf("invalid UUID in '%s' attribute (got %d bytes)", attribute, len(value))
}
return "", err
// AD stores objectGUID with mixed endianness 🤪 - swap first 3 components
if strings.EqualFold(attribute, "objectguid") {
value = swapObjectGUIDBytes(value)
}
id, err := uuid.FromBytes(value)
if err != nil {
return "", fmt.Errorf("error parsing UUID from '%s' attribute bytes: %w", attribute, err)
}
return id.String(), nil
}
return e.GetEqualFoldAttributeValue(attrType), nil
return e.GetEqualFoldAttributeValue(attribute), nil
}
func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User {

View File

@@ -455,7 +455,7 @@ func (i *LDAP) groupToLDAPAttrValues(group libregraph.Group) (map[string][]strin
}
func (i *LDAP) getLDAPGroupByID(id string, requestMembers bool) (*ldap.Entry, error) {
idString, err := filterEscapeUUID(i.groupIDisOctetString, id)
idString, err := filterEscapeAttribute(i.groupAttributeMap.id, i.groupIDisOctetString, id)
if err != nil {
return nil, fmt.Errorf("invalid group id: %w", err)
}
@@ -464,7 +464,7 @@ func (i *LDAP) getLDAPGroupByID(id string, requestMembers bool) (*ldap.Entry, er
}
func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ldap.Entry, error) {
idString, err := filterEscapeUUID(i.groupIDisOctetString, nameOrID)
idString, err := filterEscapeAttribute(i.groupAttributeMap.id, i.groupIDisOctetString, nameOrID)
// err != nil just means that this is not an uuid, so we can skip the uuid filter part
// and just filter by name
filter := ""

View File

@@ -2,6 +2,7 @@ package identity
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/url"
@@ -9,10 +10,10 @@ import (
"github.com/CiscoM31/godata"
"github.com/go-ldap/ldap/v3"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
@@ -63,6 +64,33 @@ var userEntry = ldap.NewEntry("uid=user",
"usertypeattribute": {"Member"},
})
var lconfigAD = config.LDAP{
UserBaseDN: "ou=users,dc=test",
UserObjectClass: "user",
UserSearchScope: "sub",
UserFilter: "",
UserDisplayNameAttribute: "displayname",
UserIDAttribute: "objectGUID",
UserIDIsOctetString: true,
UserEmailAttribute: "mail",
UserNameAttribute: "uid",
UserEnabledAttribute: "userEnabledAttribute",
UserTypeAttribute: "userTypeAttribute",
LdapDisabledUsersGroupDN: disableUsersGroup,
DisableUserMechanism: "attribute",
GroupBaseDN: "ou=groups,dc=test",
GroupObjectClass: "group",
GroupSearchScope: "sub",
GroupFilter: "",
GroupNameAttribute: "cn",
GroupMemberAttribute: "member",
GroupIDAttribute: "objectGUID",
GroupIDIsOctetString: true,
WriteEnabled: true,
}
var invalidUserEntry = ldap.NewEntry("uid=user",
map[string][]string{
"uid": {"invalid"},
@@ -260,6 +288,50 @@ func TestGetUser(t *testing.T) {
assert.ErrorContains(t, err, "itemNotFound:")
}
func TestGetUserAD(t *testing.T) {
// we have to simulate ldap / AD returning a binary encoded objectguid
byteID, err := base64.StdEncoding.DecodeString("js8n0m6YBUqIYK8ZMFYnig==")
if err != nil {
t.Error(err)
}
userEntryAD := ldap.NewEntry("uid=user",
map[string][]string{
"uid": {"user"},
"displayname": {"DisplayName"},
"mail": {"user@example"},
"objectguid": {string(byteID)}, // ugly but works
"sn": {"surname"},
"givenname": {"givenName"},
"userenabledattribute": {"TRUE"},
"usertypeattribute": {"Member"},
})
// Mock a valid Search Result
lm := &mocks.Client{}
lm.On("Search", mock.Anything).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{userEntryAD},
},
nil)
odataReqDefault, err := godata.ParseRequest(context.Background(), "",
url.Values{})
if err != nil {
t.Errorf("Expected success got '%s'", err.Error())
}
b, _ := getMockedBackend(lm, lconfigAD, &logger)
u, err := b.GetUser(context.Background(), "user", odataReqDefault)
if err != nil {
t.Errorf("Expected GetUser to succeed. Got %s", err.Error())
} else if *u.Id != "d227cf8e-986e-4a05-8860-af193056278a" { // this checks if we decoded the objectguid correctly
t.Errorf("Expected GetUser to return a valid user")
}
}
func TestGetUsers(t *testing.T) {
// Mock a Sizelimit Error
lm := &mocks.Client{}