fix: scim 2 filter: the username should be treated case-insensitive (#9257)

# Which Problems Are Solved
- when listing users via scim v2.0 filters applied to the username are
applied case-sensitive

# How the Problems Are Solved
- when a query filter is appleid on the username it is applied
case-insensitive

# Additional Context
Part of https://github.com/zitadel/zitadel/issues/8140
This commit is contained in:
Lars 2025-01-29 14:22:22 +01:00 committed by GitHub
parent b10428fb56
commit accfb7525a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 85 additions and 26 deletions

View File

@ -33,6 +33,7 @@ type MappedQueryBuilderFunc func(ctx context.Context, compareValue *CompValue, o
type QueryFieldInfo struct {
Column query.Column
FieldType FieldType
CaseInsensitive bool
BuildMappedQuery MappedQueryBuilderFunc
}
@ -290,19 +291,36 @@ func (b *queryBuilder) buildTextQuery(field *QueryFieldInfo, right CompValue, op
}
var comp query.TextComparison
switch {
case op.Equal:
comp = query.TextEquals
case op.NotEqual:
comp = query.TextNotEquals
case op.Contains:
comp = query.TextContains
case op.StartsWith:
comp = query.TextStartsWith
case op.EndsWith:
comp = query.TextEndsWith
default:
return nil, serrors.ThrowInvalidFilter(zerrors.ThrowInvalidArgument(nil, "SCIM-FF425", "Invalid filter expression: unsupported comparison operator for text fields"))
if field.CaseInsensitive {
switch {
case op.Equal:
comp = query.TextEqualsIgnoreCase
case op.NotEqual:
comp = query.TextNotEqualsIgnoreCase
case op.Contains:
comp = query.TextContainsIgnoreCase
case op.StartsWith:
comp = query.TextStartsWithIgnoreCase
case op.EndsWith:
comp = query.TextEndsWithIgnoreCase
default:
return nil, serrors.ThrowInvalidFilter(zerrors.ThrowInvalidArgument(nil, "SCIM-FF529", "Invalid filter expression: unsupported comparison operator for text fields"))
}
} else {
switch {
case op.Equal:
comp = query.TextEquals
case op.NotEqual:
comp = query.TextNotEquals
case op.Contains:
comp = query.TextContains
case op.StartsWith:
comp = query.TextStartsWith
case op.EndsWith:
comp = query.TextEndsWith
default:
return nil, serrors.ThrowInvalidFilter(zerrors.ThrowInvalidArgument(nil, "SCIM-FF425", "Invalid filter expression: unsupported comparison operator for text fields"))
}
}
return query.NewTextQuery(field.Column, *right.StringValue, comp)

View File

@ -20,10 +20,11 @@ var fieldPathColumnMapping = FieldPathMapping{
Column: query.UserChangeDateCol,
FieldType: FieldTypeTimestamp,
},
// a string field
// a case-insensitive string field
"username": {
Column: query.UserUsernameCol,
FieldType: FieldTypeString,
Column: query.UserUsernameCol,
FieldType: FieldTypeString,
CaseInsensitive: true,
},
// a nested string field
"name.familyname": {
@ -76,7 +77,7 @@ func TestFilter_BuildQuery(t *testing.T) {
{
name: "simple binary operator",
filter: `userName eq "bjensen"`,
want: test.Must(query.NewTextQuery(query.UserUsernameCol, "bjensen", query.TextEquals)),
want: test.Must(query.NewTextQuery(query.UserUsernameCol, "bjensen", query.TextEqualsIgnoreCase)),
},
{
name: "binary operator equals null",
@ -176,7 +177,7 @@ func TestFilter_BuildQuery(t *testing.T) {
{
name: "urn prefixed binary operator",
filter: `urn:ietf:params:scim:schemas:core:2.0:User:userName sw "J"`,
want: test.Must(query.NewTextQuery(query.UserUsernameCol, "J", query.TextStartsWith)),
want: test.Must(query.NewTextQuery(query.UserUsernameCol, "J", query.TextStartsWithIgnoreCase)),
},
{
name: "urn prefixed nested binary operator",
@ -196,7 +197,7 @@ func TestFilter_BuildQuery(t *testing.T) {
{
name: "and logical expression",
filter: `name.familyName pr and userName eq "bjensen"`,
want: test.Must(query.NewAndQuery(test.Must(query.NewNotNullQuery(query.HumanLastNameCol)), test.Must(query.NewTextQuery(query.UserUsernameCol, "bjensen", query.TextEquals)))),
want: test.Must(query.NewAndQuery(test.Must(query.NewNotNullQuery(query.HumanLastNameCol)), test.Must(query.NewTextQuery(query.UserUsernameCol, "bjensen", query.TextEqualsIgnoreCase)))),
},
{
name: "timestamp condition equal",
@ -243,7 +244,7 @@ func TestFilter_BuildQuery(t *testing.T) {
filter: `userName eq "rudolpho" and emails co "example.com" or emails.value co "example2.org"`,
want: test.Must(query.NewOrQuery(
test.Must(query.NewAndQuery(
test.Must(query.NewTextQuery(query.UserUsernameCol, "rudolpho", query.TextEquals)),
test.Must(query.NewTextQuery(query.UserUsernameCol, "rudolpho", query.TextEqualsIgnoreCase)),
test.Must(query.NewTextQuery(query.HumanEmailCol, "example.com", query.TextContains))),
),
test.Must(query.NewTextQuery(query.HumanEmailCol, "example2.org", query.TextContains)))),
@ -252,7 +253,7 @@ func TestFilter_BuildQuery(t *testing.T) {
name: "nested and / or with grouping",
filter: `userName ne "rudolpho" and (emails co "example.com" or emails.value co "example.org")`,
want: test.Must(query.NewAndQuery(
test.Must(query.NewTextQuery(query.UserUsernameCol, "rudolpho", query.TextNotEquals)),
test.Must(query.NewTextQuery(query.UserUsernameCol, "rudolpho", query.TextNotEqualsIgnoreCase)),
test.Must(query.NewOrQuery(
test.Must(query.NewTextQuery(query.HumanEmailCol, "example.com", query.TextContains)),
test.Must(query.NewTextQuery(query.HumanEmailCol, "example.org", query.TextContains)),
@ -263,7 +264,7 @@ func TestFilter_BuildQuery(t *testing.T) {
name: "nested value path path",
filter: `userName eq "Hans" and emails[value ew "@example.org" or value ew "@example.com"]`,
want: test.Must(query.NewAndQuery(
test.Must(query.NewTextQuery(query.UserUsernameCol, "Hans", query.TextEquals)),
test.Must(query.NewTextQuery(query.UserUsernameCol, "Hans", query.TextEqualsIgnoreCase)),
test.Must(query.NewOrQuery(
test.Must(query.NewTextQuery(query.HumanEmailCol, "@example.org", query.TextEndsWith)),
test.Must(query.NewTextQuery(query.HumanEmailCol, "@example.com", query.TextEndsWith)),
@ -288,13 +289,13 @@ func TestFilter_BuildQuery(t *testing.T) {
want: test.Must(query.NewAndQuery(
test.Must(query.NewTextQuery(query.HumanEmailCol, "@example.com", query.TextEndsWith)),
test.Must(query.NewTextQuery(query.HumanLastNameCol, "hans", query.TextContains)),
test.Must(query.NewTextQuery(query.UserUsernameCol, "peter", query.TextContains)),
test.Must(query.NewTextQuery(query.UserUsernameCol, "peter", query.TextContainsIgnoreCase)),
)),
},
{
name: "negation",
filter: `not(username eq "foo")`,
want: test.Must(query.NewNotQuery(test.Must(query.NewTextQuery(query.UserUsernameCol, "foo", query.TextEquals)))),
want: test.Must(query.NewNotQuery(test.Must(query.NewTextQuery(query.UserUsernameCol, "foo", query.TextEqualsIgnoreCase)))),
},
{
name: "negation with complex filter",

View File

@ -29,8 +29,9 @@ var fieldPathColumnMapping = filter.FieldPathMapping{
FieldType: filter.FieldTypeString,
},
"username": {
Column: query.UserUsernameCol,
FieldType: filter.FieldTypeString,
Column: query.UserUsernameCol,
FieldType: filter.FieldTypeString,
CaseInsensitive: true,
},
"name.familyname": {
Column: query.HumanLastNameCol,

View File

@ -288,6 +288,7 @@ func NewTextQuery(col Column, value string, compare TextComparison) (*textQuery,
// handle the comparisons which use (i)like and therefore need to escape potential wildcards in the value
switch compare {
case TextEqualsIgnoreCase,
TextNotEqualsIgnoreCase,
TextStartsWith,
TextStartsWithIgnoreCase,
TextEndsWith,
@ -334,6 +335,8 @@ func (q *textQuery) comp() sq.Sqlizer {
return sq.NotEq{q.Column.identifier(): q.Text}
case TextEqualsIgnoreCase:
return sq.ILike{q.Column.identifier(): q.Text}
case TextNotEqualsIgnoreCase:
return sq.NotILike{q.Column.identifier(): q.Text}
case TextStartsWith:
return sq.Like{q.Column.identifier(): q.Text + "%"}
case TextStartsWithIgnoreCase:
@ -368,6 +371,7 @@ const (
TextContainsIgnoreCase
TextListContains
TextNotEquals
TextNotEqualsIgnoreCase
textCompareMax
)

View File

@ -905,6 +905,19 @@ func TestNewTextQuery(t *testing.T) {
Compare: TextNotEquals,
},
},
{
name: "not equal ignore case",
args: args{
column: testCol,
value: "h_urst%",
compare: TextNotEqualsIgnoreCase,
},
want: &textQuery{
Column: testCol,
Text: "h\\_urst\\%",
Compare: TextNotEqualsIgnoreCase,
},
},
{
name: "starts with",
args: args{
@ -1194,6 +1207,28 @@ func TestTextQuery_comp(t *testing.T) {
query: sq.ILike{"test_table.test_col": "Hurst"},
},
},
{
name: "not equals",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextNotEquals,
},
want: want{
query: sq.NotEq{"test_table.test_col": "Hurst"},
},
},
{
name: "not equals ignore case",
fields: fields{
Column: testCol,
Text: "Hurst",
Compare: TextNotEqualsIgnoreCase,
},
want: want{
query: sq.NotILike{"test_table.test_col": "Hurst"},
},
},
{
name: "equals ignore case wildcard",
fields: fields{