mirror of
https://github.com/zitadel/zitadel.git
synced 2025-05-06 12:16:46 +00:00
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:
parent
b10428fb56
commit
accfb7525a
@ -33,6 +33,7 @@ type MappedQueryBuilderFunc func(ctx context.Context, compareValue *CompValue, o
|
|||||||
type QueryFieldInfo struct {
|
type QueryFieldInfo struct {
|
||||||
Column query.Column
|
Column query.Column
|
||||||
FieldType FieldType
|
FieldType FieldType
|
||||||
|
CaseInsensitive bool
|
||||||
BuildMappedQuery MappedQueryBuilderFunc
|
BuildMappedQuery MappedQueryBuilderFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,19 +291,36 @@ func (b *queryBuilder) buildTextQuery(field *QueryFieldInfo, right CompValue, op
|
|||||||
}
|
}
|
||||||
|
|
||||||
var comp query.TextComparison
|
var comp query.TextComparison
|
||||||
switch {
|
if field.CaseInsensitive {
|
||||||
case op.Equal:
|
switch {
|
||||||
comp = query.TextEquals
|
case op.Equal:
|
||||||
case op.NotEqual:
|
comp = query.TextEqualsIgnoreCase
|
||||||
comp = query.TextNotEquals
|
case op.NotEqual:
|
||||||
case op.Contains:
|
comp = query.TextNotEqualsIgnoreCase
|
||||||
comp = query.TextContains
|
case op.Contains:
|
||||||
case op.StartsWith:
|
comp = query.TextContainsIgnoreCase
|
||||||
comp = query.TextStartsWith
|
case op.StartsWith:
|
||||||
case op.EndsWith:
|
comp = query.TextStartsWithIgnoreCase
|
||||||
comp = query.TextEndsWith
|
case op.EndsWith:
|
||||||
default:
|
comp = query.TextEndsWithIgnoreCase
|
||||||
return nil, serrors.ThrowInvalidFilter(zerrors.ThrowInvalidArgument(nil, "SCIM-FF425", "Invalid filter expression: unsupported comparison operator for text fields"))
|
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)
|
return query.NewTextQuery(field.Column, *right.StringValue, comp)
|
||||||
|
@ -20,10 +20,11 @@ var fieldPathColumnMapping = FieldPathMapping{
|
|||||||
Column: query.UserChangeDateCol,
|
Column: query.UserChangeDateCol,
|
||||||
FieldType: FieldTypeTimestamp,
|
FieldType: FieldTypeTimestamp,
|
||||||
},
|
},
|
||||||
// a string field
|
// a case-insensitive string field
|
||||||
"username": {
|
"username": {
|
||||||
Column: query.UserUsernameCol,
|
Column: query.UserUsernameCol,
|
||||||
FieldType: FieldTypeString,
|
FieldType: FieldTypeString,
|
||||||
|
CaseInsensitive: true,
|
||||||
},
|
},
|
||||||
// a nested string field
|
// a nested string field
|
||||||
"name.familyname": {
|
"name.familyname": {
|
||||||
@ -76,7 +77,7 @@ func TestFilter_BuildQuery(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "simple binary operator",
|
name: "simple binary operator",
|
||||||
filter: `userName eq "bjensen"`,
|
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",
|
name: "binary operator equals null",
|
||||||
@ -176,7 +177,7 @@ func TestFilter_BuildQuery(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "urn prefixed binary operator",
|
name: "urn prefixed binary operator",
|
||||||
filter: `urn:ietf:params:scim:schemas:core:2.0:User:userName sw "J"`,
|
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",
|
name: "urn prefixed nested binary operator",
|
||||||
@ -196,7 +197,7 @@ func TestFilter_BuildQuery(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "and logical expression",
|
name: "and logical expression",
|
||||||
filter: `name.familyName pr and userName eq "bjensen"`,
|
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",
|
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"`,
|
filter: `userName eq "rudolpho" and emails co "example.com" or emails.value co "example2.org"`,
|
||||||
want: test.Must(query.NewOrQuery(
|
want: test.Must(query.NewOrQuery(
|
||||||
test.Must(query.NewAndQuery(
|
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, "example.com", query.TextContains))),
|
||||||
),
|
),
|
||||||
test.Must(query.NewTextQuery(query.HumanEmailCol, "example2.org", 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",
|
name: "nested and / or with grouping",
|
||||||
filter: `userName ne "rudolpho" and (emails co "example.com" or emails.value co "example.org")`,
|
filter: `userName ne "rudolpho" and (emails co "example.com" or emails.value co "example.org")`,
|
||||||
want: test.Must(query.NewAndQuery(
|
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.NewOrQuery(
|
||||||
test.Must(query.NewTextQuery(query.HumanEmailCol, "example.com", query.TextContains)),
|
test.Must(query.NewTextQuery(query.HumanEmailCol, "example.com", query.TextContains)),
|
||||||
test.Must(query.NewTextQuery(query.HumanEmailCol, "example.org", 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",
|
name: "nested value path path",
|
||||||
filter: `userName eq "Hans" and emails[value ew "@example.org" or value ew "@example.com"]`,
|
filter: `userName eq "Hans" and emails[value ew "@example.org" or value ew "@example.com"]`,
|
||||||
want: test.Must(query.NewAndQuery(
|
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.NewOrQuery(
|
||||||
test.Must(query.NewTextQuery(query.HumanEmailCol, "@example.org", query.TextEndsWith)),
|
test.Must(query.NewTextQuery(query.HumanEmailCol, "@example.org", query.TextEndsWith)),
|
||||||
test.Must(query.NewTextQuery(query.HumanEmailCol, "@example.com", 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(
|
want: test.Must(query.NewAndQuery(
|
||||||
test.Must(query.NewTextQuery(query.HumanEmailCol, "@example.com", query.TextEndsWith)),
|
test.Must(query.NewTextQuery(query.HumanEmailCol, "@example.com", query.TextEndsWith)),
|
||||||
test.Must(query.NewTextQuery(query.HumanLastNameCol, "hans", query.TextContains)),
|
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",
|
name: "negation",
|
||||||
filter: `not(username eq "foo")`,
|
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",
|
name: "negation with complex filter",
|
||||||
|
@ -29,8 +29,9 @@ var fieldPathColumnMapping = filter.FieldPathMapping{
|
|||||||
FieldType: filter.FieldTypeString,
|
FieldType: filter.FieldTypeString,
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
Column: query.UserUsernameCol,
|
Column: query.UserUsernameCol,
|
||||||
FieldType: filter.FieldTypeString,
|
FieldType: filter.FieldTypeString,
|
||||||
|
CaseInsensitive: true,
|
||||||
},
|
},
|
||||||
"name.familyname": {
|
"name.familyname": {
|
||||||
Column: query.HumanLastNameCol,
|
Column: query.HumanLastNameCol,
|
||||||
|
@ -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
|
// handle the comparisons which use (i)like and therefore need to escape potential wildcards in the value
|
||||||
switch compare {
|
switch compare {
|
||||||
case TextEqualsIgnoreCase,
|
case TextEqualsIgnoreCase,
|
||||||
|
TextNotEqualsIgnoreCase,
|
||||||
TextStartsWith,
|
TextStartsWith,
|
||||||
TextStartsWithIgnoreCase,
|
TextStartsWithIgnoreCase,
|
||||||
TextEndsWith,
|
TextEndsWith,
|
||||||
@ -334,6 +335,8 @@ func (q *textQuery) comp() sq.Sqlizer {
|
|||||||
return sq.NotEq{q.Column.identifier(): q.Text}
|
return sq.NotEq{q.Column.identifier(): q.Text}
|
||||||
case TextEqualsIgnoreCase:
|
case TextEqualsIgnoreCase:
|
||||||
return sq.ILike{q.Column.identifier(): q.Text}
|
return sq.ILike{q.Column.identifier(): q.Text}
|
||||||
|
case TextNotEqualsIgnoreCase:
|
||||||
|
return sq.NotILike{q.Column.identifier(): q.Text}
|
||||||
case TextStartsWith:
|
case TextStartsWith:
|
||||||
return sq.Like{q.Column.identifier(): q.Text + "%"}
|
return sq.Like{q.Column.identifier(): q.Text + "%"}
|
||||||
case TextStartsWithIgnoreCase:
|
case TextStartsWithIgnoreCase:
|
||||||
@ -368,6 +371,7 @@ const (
|
|||||||
TextContainsIgnoreCase
|
TextContainsIgnoreCase
|
||||||
TextListContains
|
TextListContains
|
||||||
TextNotEquals
|
TextNotEquals
|
||||||
|
TextNotEqualsIgnoreCase
|
||||||
|
|
||||||
textCompareMax
|
textCompareMax
|
||||||
)
|
)
|
||||||
|
@ -905,6 +905,19 @@ func TestNewTextQuery(t *testing.T) {
|
|||||||
Compare: TextNotEquals,
|
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",
|
name: "starts with",
|
||||||
args: args{
|
args: args{
|
||||||
@ -1194,6 +1207,28 @@ func TestTextQuery_comp(t *testing.T) {
|
|||||||
query: sq.ILike{"test_table.test_col": "Hurst"},
|
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",
|
name: "equals ignore case wildcard",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user