mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-23 22:29:59 -05:00
Merge pull request #1901 from opencloud-eu/handle-objectguid-endianness
handle objectguid endianess
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 := ""
|
||||
|
||||
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user