outposts/ldap: Handle comma-separated attributes in LDAP search requests (#15000)
Closes https://github.com/goauthentik/authentik/issues/13539 When LDAP clients like Jira submit search requests with comma-separated attributes (e.g., ["uid,cn,sn"] instead of ["uid", "cn", "sn"]), the LDAP outpost would return an "Operations Error". Ths fix adds attribute normalization to properly handle both formats by splitting comma separated attributes into individual entries. Tests pass: ``` === RUN TestNormalizeAttributes === RUN TestNormalizeAttributes/Empty_input === RUN TestNormalizeAttributes/No_commas === RUN TestNormalizeAttributes/Single_comma-separated_string === RUN TestNormalizeAttributes/Mixed_input === RUN TestNormalizeAttributes/With_spaces === RUN TestNormalizeAttributes/Empty_parts === RUN TestNormalizeAttributes/Single_element === RUN TestNormalizeAttributes/Only_commas === RUN TestNormalizeAttributes/Multiple_comma-separated_attributes === RUN TestNormalizeAttributes/Case_preservation === RUN TestNormalizeAttributes/Leading_and_trailing_spaces === RUN TestNormalizeAttributes/Real-world_LDAP_attribute_examples === RUN TestNormalizeAttributes/Jira-style_attribute_format === RUN TestNormalizeAttributes/Single_string_with_single_attribute === RUN TestNormalizeAttributes/Mix_of_standard_and_operational_attributes --- PASS: TestNormalizeAttributes (0.00s) --- PASS: TestNormalizeAttributes/Empty_input (0.00s) --- PASS: TestNormalizeAttributes/No_commas (0.00s) --- PASS: TestNormalizeAttributes/Single_comma-separated_string (0.00s) --- PASS: TestNormalizeAttributes/Mixed_input (0.00s) --- PASS: TestNormalizeAttributes/With_spaces (0.00s) --- PASS: TestNormalizeAttributes/Empty_parts (0.00s) --- PASS: TestNormalizeAttributes/Single_element (0.00s) --- PASS: TestNormalizeAttributes/Only_commas (0.00s) --- PASS: TestNormalizeAttributes/Multiple_comma-separated_attributes (0.00s) --- PASS: TestNormalizeAttributes/Case_preservation (0.00s) --- PASS: TestNormalizeAttributes/Leading_and_trailing_spaces (0.00s) --- PASS: TestNormalizeAttributes/Real-world_LDAP_attribute_examples (0.00s) --- PASS: TestNormalizeAttributes/Jira-style_attribute_format (0.00s) --- PASS: TestNormalizeAttributes/Single_string_with_single_attribute (0.00s) --- PASS: TestNormalizeAttributes/Mix_of_standard_and_operational_attributes (0.00s) PASS ok goauthentik.io/internal/outpost/ldap/search 0.194s ```
This commit is contained in:
@ -53,6 +53,14 @@ func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Re
|
|||||||
if err != nil && len(searchReq.Filter) > 0 {
|
if err != nil && len(searchReq.Filter) > 0 {
|
||||||
l.WithError(err).WithField("objectClass", filterOC).Warning("invalid filter object class")
|
l.WithError(err).WithField("objectClass", filterOC).Warning("invalid filter object class")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle comma-separated attributes
|
||||||
|
normalizedAttributes := normalizeAttributes(searchReq.Attributes)
|
||||||
|
if len(normalizedAttributes) != len(searchReq.Attributes) {
|
||||||
|
// Create a copy of the search request with normalized attributes
|
||||||
|
searchReq.Attributes = normalizedAttributes
|
||||||
|
}
|
||||||
|
|
||||||
return &Request{
|
return &Request{
|
||||||
SearchRequest: searchReq,
|
SearchRequest: searchReq,
|
||||||
BindDN: bindDN,
|
BindDN: bindDN,
|
||||||
@ -64,6 +72,31 @@ func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Re
|
|||||||
}, span
|
}, span
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeAttributes handles the case where attributes might be passed as comma-separated strings
|
||||||
|
// rather than as individual array elements
|
||||||
|
func normalizeAttributes(attributes []string) []string {
|
||||||
|
if len(attributes) == 0 {
|
||||||
|
return attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]string, 0, len(attributes))
|
||||||
|
for _, attr := range attributes {
|
||||||
|
if strings.Contains(attr, ",") {
|
||||||
|
// Split comma-separated attributes and add them individually
|
||||||
|
parts := strings.Split(attr, ",")
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part != "" {
|
||||||
|
result = append(result, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = append(result, attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Request) Context() context.Context {
|
func (r *Request) Context() context.Context {
|
||||||
return r.ctx
|
return r.ctx
|
||||||
}
|
}
|
||||||
|
|||||||
98
internal/outpost/ldap/search/request_test.go
Normal file
98
internal/outpost/ldap/search/request_test.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeAttributes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []string
|
||||||
|
expectedOutput []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty input",
|
||||||
|
input: []string{},
|
||||||
|
expectedOutput: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No commas",
|
||||||
|
input: []string{"uid", "cn", "sn"},
|
||||||
|
expectedOutput: []string{"uid", "cn", "sn"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single comma-separated string",
|
||||||
|
input: []string{"uid,cn,sn"},
|
||||||
|
expectedOutput: []string{"uid", "cn", "sn"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mixed input",
|
||||||
|
input: []string{"uid,cn", "sn"},
|
||||||
|
expectedOutput: []string{"uid", "cn", "sn"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With spaces",
|
||||||
|
input: []string{"uid, cn, sn"},
|
||||||
|
expectedOutput: []string{"uid", "cn", "sn"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty parts",
|
||||||
|
input: []string{"uid,, cn"},
|
||||||
|
expectedOutput: []string{"uid", "cn"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single element",
|
||||||
|
input: []string{"uid"},
|
||||||
|
expectedOutput: []string{"uid"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Only commas",
|
||||||
|
input: []string{",,,"},
|
||||||
|
expectedOutput: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple comma-separated attributes",
|
||||||
|
input: []string{"uid,cn", "sn,mail", "givenName"},
|
||||||
|
expectedOutput: []string{"uid", "cn", "sn", "mail", "givenName"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Case preservation",
|
||||||
|
input: []string{"uid,CN,sAMAccountName"},
|
||||||
|
expectedOutput: []string{"uid", "CN", "sAMAccountName"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Leading and trailing spaces",
|
||||||
|
input: []string{" uid , cn , sn "},
|
||||||
|
expectedOutput: []string{"uid", "cn", "sn"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Real-world LDAP attribute examples",
|
||||||
|
input: []string{"objectClass,memberOf,mail", "sAMAccountName,userPrincipalName"},
|
||||||
|
expectedOutput: []string{"objectClass", "memberOf", "mail", "sAMAccountName", "userPrincipalName"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jira-style attribute format",
|
||||||
|
input: []string{"uid,cn,sn"},
|
||||||
|
expectedOutput: []string{"uid", "cn", "sn"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single string with single attribute",
|
||||||
|
input: []string{"cn"},
|
||||||
|
expectedOutput: []string{"cn"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mix of standard and operational attributes",
|
||||||
|
input: []string{"uid,+", "createTimestamp"},
|
||||||
|
expectedOutput: []string{"uid", "+", "createTimestamp"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := normalizeAttributes(tt.input)
|
||||||
|
assert.Equal(t, tt.expectedOutput, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user