diff --git a/errors/error.go b/errors/error.go index fac0643..2a8770c 100644 --- a/errors/error.go +++ b/errors/error.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "slices" "strconv" "strings" ) @@ -113,12 +114,7 @@ func checkApplicability(err ScimError, method string) bool { return false } - for _, m := range methods { - if m == method { - return true - } - } - return false + return slices.Contains(methods, method) } // ScimError is a SCIM error response to indicate operation success or failure. diff --git a/filter/filter.go b/filter/filter.go index fc12719..590160b 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -117,7 +117,7 @@ func (v Validator) GetFilter() filter.Expression { } // PassesFilter checks whether given resources passes the filter. -func (v Validator) PassesFilter(resource map[string]interface{}) error { +func (v Validator) PassesFilter(resource map[string]any) error { switch e := v.filter.(type) { case *filter.ValuePath: ref, attr, ok := v.referenceContains(e.AttributePath) @@ -144,9 +144,9 @@ func (v Validator) PassesFilter(resource map[string]interface{}) error { }, } switch value := value.(type) { - case []interface{}: + case []any: for _, a := range value { - attr, ok := a.(map[string]interface{}) + attr, ok := a.(map[string]any) if !ok { return fmt.Errorf("the target is not a complex attribute") } @@ -190,7 +190,7 @@ func (v Validator) PassesFilter(resource map[string]interface{}) error { return fmt.Errorf("the resource has no sub-attribute named: %s", subAttrName) } - attr, ok := value.(map[string]interface{}) + attr, ok := value.(map[string]any) if !ok { return fmt.Errorf("the target is not a complex attribute") } @@ -221,7 +221,7 @@ func (v Validator) PassesFilter(resource map[string]interface{}) error { } switch value := value.(type) { - case []interface{}: + case []any: var err error for _, v := range value { if err = cmp(v); err == nil { diff --git a/filter/filter_test.go b/filter/filter_test.go index 62293f6..d0c9872 100644 --- a/filter/filter_test.go +++ b/filter/filter_test.go @@ -62,30 +62,30 @@ func TestValidator_PassesFilter(t *testing.T) { t.Run("simple", func(t *testing.T) { for _, test := range []struct { filter string - valid map[string]interface{} - invalid map[string]interface{} + valid map[string]any + invalid map[string]any }{ { filter: `userName eq "john"`, - valid: map[string]interface{}{ + valid: map[string]any{ "userName": "john", }, - invalid: map[string]interface{}{ + invalid: map[string]any{ "userName": "doe", }, }, { filter: `emails[type eq "work"]`, - valid: map[string]interface{}{ - "emails": []interface{}{ - map[string]interface{}{ + valid: map[string]any{ + "emails": []any{ + map[string]any{ "type": "work", }, }, }, - invalid: map[string]interface{}{ - "emails": []interface{}{ - map[string]interface{}{ + invalid: map[string]any{ + "emails": []any{ + map[string]any{ "type": "private", }, }, @@ -222,36 +222,36 @@ func TestValidator_Validate(t *testing.T) { } } -func testResources() []map[string]interface{} { - return []map[string]interface{}{ +func testResources() []map[string]any { + return []map[string]any{ { - "schemas": []interface{}{ + "schemas": []any{ "urn:ietf:params:scim:schemas:core:2.0:User", }, "userName": "di-wu", "userType": "admin", - "name": map[string]interface{}{ + "name": map[string]any{ "familyName": "di", "givenName": "wu", }, - "emails": []interface{}{ - map[string]interface{}{ + "emails": []any{ + map[string]any{ "value": "quint@elimity.com", "type": "work", }, }, - "meta": map[string]interface{}{ + "meta": map[string]any{ "lastModified": "2020-07-26T20:02:34Z", }, "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization": "Elimity", }, { - "schemas": []interface{}{ + "schemas": []any{ "urn:ietf:params:scim:schemas:core:2.0:User", }, "userName": "noreply", - "emails": []interface{}{ - map[string]interface{}{ + "emails": []any{ + map[string]any{ "value": "noreply@elimity.com", "type": "work", }, @@ -260,18 +260,18 @@ func testResources() []map[string]interface{} { { "userName": "admin", "userType": "admin", - "name": map[string]interface{}{ + "name": map[string]any{ "familyName": "ad", "givenName": "min", }, - "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager": map[string]interface{}{ + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager": map[string]any{ "displayName": "di-wu", }, }, {"userName": "guest"}, { "userName": "unknown", - "name": map[string]interface{}{ + "name": map[string]any{ "familyName": "un", "givenName": "known", }, diff --git a/filter/op_binary.go b/filter/op_binary.go index 741753b..692091a 100644 --- a/filter/op_binary.go +++ b/filter/op_binary.go @@ -11,7 +11,7 @@ import ( // // Expects a binary attribute. Will panic on unknown filter operator. // Known operators: eq, ne, co, sw, ew, gt, lt, ge and le. -func cmpBinary(e *filter.AttributeExpression, ref string) (func(interface{}) error, error) { +func cmpBinary(e *filter.AttributeExpression, ref string) (func(any) error, error) { switch op := e.Operator; op { case filter.EQ: return cmpStr(ref, true, func(v, ref string) error { diff --git a/filter/op_boolean.go b/filter/op_boolean.go index cb51e93..cb638c0 100644 --- a/filter/op_boolean.go +++ b/filter/op_boolean.go @@ -7,8 +7,8 @@ import ( "strings" ) -func cmpBool(ref bool, cmp func(v, ref bool) error) func(interface{}) error { - return func(i interface{}) error { +func cmpBool(ref bool, cmp func(v, ref bool) error) func(any) error { + return func(i any) error { value, ok := i.(bool) if !ok { panic(fmt.Sprintf("given value is not a boolean: %v", i)) @@ -17,8 +17,8 @@ func cmpBool(ref bool, cmp func(v, ref bool) error) func(interface{}) error { } } -func cmpBoolStr(ref bool, cmp func(v, ref string) error) (func(interface{}) error, error) { - return func(i interface{}) error { +func cmpBoolStr(ref bool, cmp func(v, ref string) error) (func(any) error, error) { + return func(i any) error { if _, ok := i.(bool); !ok { panic(fmt.Sprintf("given value is not a boolean: %v", i)) } @@ -32,7 +32,7 @@ func cmpBoolStr(ref bool, cmp func(v, ref string) error) (func(interface{}) erro // // Expects a boolean attribute. Will panic on unknown filter operator. // Known operators: eq, ne, co, sw, ew, gt, lt, ge and le. -func cmpBoolean(e *filter.AttributeExpression, attr schema.CoreAttribute, ref bool) (func(interface{}) error, error) { +func cmpBoolean(e *filter.AttributeExpression, attr schema.CoreAttribute, ref bool) (func(any) error, error) { switch op := e.Operator; op { case filter.EQ: return cmpBool(ref, func(v, ref bool) error { diff --git a/filter/op_boolean_test.go b/filter/op_boolean_test.go index b46eda9..1f15ebf 100644 --- a/filter/op_boolean_test.go +++ b/filter/op_boolean_test.go @@ -20,7 +20,7 @@ func TestValidatorBoolean(t *testing.T) { })), }, } - attr = map[string]interface{}{ + attr = map[string]any{ "bool": true, } ) diff --git a/filter/op_datetime.go b/filter/op_datetime.go index 0e9aa00..ffb2e41 100644 --- a/filter/op_datetime.go +++ b/filter/op_datetime.go @@ -13,7 +13,7 @@ import ( // // Expects a dateTime attribute. Will panic on unknown filter operator. // Known operators: eq, ne, co, sw, ew, gt, lt, ge and le. -func cmpDateTime(e *filter.AttributeExpression, date string, ref time.Time) (func(interface{}) error, error) { +func cmpDateTime(e *filter.AttributeExpression, date string, ref time.Time) (func(any) error, error) { switch op := e.Operator; op { case filter.EQ: return cmpTime(ref, func(v, ref time.Time) error { @@ -83,8 +83,8 @@ func cmpDateTime(e *filter.AttributeExpression, date string, ref time.Time) (fun } } -func cmpTime(ref time.Time, cmp func(v, ref time.Time) error) func(interface{}) error { - return func(i interface{}) error { +func cmpTime(ref time.Time, cmp func(v, ref time.Time) error) func(any) error { + return func(i any) error { date, ok := i.(string) if !ok { panic(fmt.Sprintf("given value is not a string: %v", i)) diff --git a/filter/op_datetime_test.go b/filter/op_datetime_test.go index 749937a..899535c 100644 --- a/filter/op_datetime_test.go +++ b/filter/op_datetime_test.go @@ -20,7 +20,7 @@ func TestValidatorDateTime(t *testing.T) { })), }, } - attrs = [3]map[string]interface{}{ + attrs = [3]map[string]any{ {"time": "2021-01-01T08:00:00Z"}, // before {"time": "2021-01-01T12:00:00Z"}, // equal {"time": "2021-01-01T16:00:00Z"}, // after diff --git a/filter/op_decimal.go b/filter/op_decimal.go index 73a58fa..dc8ee31 100644 --- a/filter/op_decimal.go +++ b/filter/op_decimal.go @@ -11,7 +11,7 @@ import ( // // Expects a decimal attribute. Will panic on unknown filter operator. // Known operators: eq, ne, co, sw, ew, gt, lt, ge and le. -func cmpDecimal(e *filter.AttributeExpression, ref float64) (func(interface{}) error, error) { +func cmpDecimal(e *filter.AttributeExpression, ref float64) (func(any) error, error) { switch op := e.Operator; op { case filter.EQ: return cmpFloat(ref, func(v, ref float64) error { @@ -81,8 +81,8 @@ func cmpDecimal(e *filter.AttributeExpression, ref float64) (func(interface{}) e } } -func cmpFloat(ref float64, cmp func(v, ref float64) error) func(interface{}) error { - return func(i interface{}) error { +func cmpFloat(ref float64, cmp func(v, ref float64) error) func(any) error { + return func(i any) error { f, ok := i.(float64) if !ok { panic(fmt.Sprintf("given value is not a float: %v", i)) @@ -91,8 +91,8 @@ func cmpFloat(ref float64, cmp func(v, ref float64) error) func(interface{}) err } } -func cmpFloatStr(ref float64, cmp func(v, ref string) error) (func(interface{}) error, error) { - return func(i interface{}) error { +func cmpFloatStr(ref float64, cmp func(v, ref string) error) (func(any) error, error) { + return func(i any) error { if _, ok := i.(float64); !ok { panic(fmt.Sprintf("given value is not a float: %v", i)) } diff --git a/filter/op_decimal_test.go b/filter/op_decimal_test.go index 8eb023e..f73b4d5 100644 --- a/filter/op_decimal_test.go +++ b/filter/op_decimal_test.go @@ -21,7 +21,7 @@ func TestValidatorDecimal(t *testing.T) { })), }, } - attrs = [3]map[string]interface{}{ + attrs = [3]map[string]any{ {"dec": -0.1}, // less {"dec": float64(1)}, // equal {"dec": 1.1}, // greater diff --git a/filter/op_integer.go b/filter/op_integer.go index f1c622a..3dbe356 100644 --- a/filter/op_integer.go +++ b/filter/op_integer.go @@ -6,8 +6,8 @@ import ( "strings" ) -func cmpInt(ref int, cmp func(v, ref int) error) func(interface{}) error { - return func(i interface{}) error { +func cmpInt(ref int, cmp func(v, ref int) error) func(any) error { + return func(i any) error { v, ok := i.(int) if !ok { panic(fmt.Sprintf("given value is not an integer: %v", i)) @@ -16,8 +16,8 @@ func cmpInt(ref int, cmp func(v, ref int) error) func(interface{}) error { } } -func cmpIntStr(ref int, cmp func(v, ref string) error) (func(interface{}) error, error) { - return func(i interface{}) error { +func cmpIntStr(ref int, cmp func(v, ref string) error) (func(any) error, error) { + return func(i any) error { if _, ok := i.(int); !ok { panic(fmt.Sprintf("given value is not an integer: %v", i)) } @@ -30,7 +30,7 @@ func cmpIntStr(ref int, cmp func(v, ref string) error) (func(interface{}) error, // // Expects a integer attribute. Will panic on unknown filter operator. // Known operators: eq, ne, co, sw, ew, gt, lt, ge and le. -func cmpInteger(e *filter.AttributeExpression, ref int) (func(interface{}) error, error) { +func cmpInteger(e *filter.AttributeExpression, ref int) (func(any) error, error) { switch op := e.Operator; op { case filter.EQ: return cmpInt(ref, func(v, ref int) error { diff --git a/filter/op_integer_test.go b/filter/op_integer_test.go index 3a72386..10cc142 100644 --- a/filter/op_integer_test.go +++ b/filter/op_integer_test.go @@ -21,7 +21,7 @@ func TestValidatorInteger(t *testing.T) { })), }, } - attrs = [3]map[string]interface{}{ + attrs = [3]map[string]any{ {"int": -1}, // less {"int": 0}, // equal {"int": 10}, // greater diff --git a/filter/op_string.go b/filter/op_string.go index 2f61c14..f4afa06 100644 --- a/filter/op_string.go +++ b/filter/op_string.go @@ -7,9 +7,9 @@ import ( "strings" ) -func cmpStr(ref string, caseExact bool, cmp func(v, ref string) error) (func(interface{}) error, error) { +func cmpStr(ref string, caseExact bool, cmp func(v, ref string) error) (func(any) error, error) { if caseExact { - return func(i interface{}) error { + return func(i any) error { value, ok := i.(string) if !ok { panic(fmt.Sprintf("given value is not a string: %v", i)) @@ -17,7 +17,7 @@ func cmpStr(ref string, caseExact bool, cmp func(v, ref string) error) (func(int return cmp(value, ref) }, nil } - return func(i interface{}) error { + return func(i any) error { value, ok := i.(string) if !ok { panic(fmt.Sprintf("given value is not a string: %v", i)) @@ -31,7 +31,7 @@ func cmpStr(ref string, caseExact bool, cmp func(v, ref string) error) (func(int // // Expects a string/reference attribute. Will panic on unknown filter operator. // Known operators: eq, ne, co, sw, ew, gt, lt, ge and le. -func cmpString(e *filter.AttributeExpression, attr schema.CoreAttribute, ref string) (func(interface{}) error, error) { +func cmpString(e *filter.AttributeExpression, attr schema.CoreAttribute, ref string) (func(any) error, error) { switch op := e.Operator; op { case filter.EQ: return cmpStr(ref, attr.CaseExact(), func(v, ref string) error { diff --git a/filter/op_string_test.go b/filter/op_string_test.go index e8b7d5e..48c116f 100644 --- a/filter/op_string_test.go +++ b/filter/op_string_test.go @@ -13,7 +13,7 @@ func TestValidatorString(t *testing.T) { exp = func(op filter.CompareOperator) string { return fmt.Sprintf("str %s \"x\"", op) } - attrs = [3]map[string]interface{}{ + attrs = [3]map[string]any{ {"str": "x"}, {"str": "X"}, {"str": "y"}, diff --git a/filter/operators.go b/filter/operators.go index 21b463f..b42e4bc 100644 --- a/filter/operators.go +++ b/filter/operators.go @@ -9,7 +9,7 @@ import ( // createCompareFunction returns a compare function based on the attribute expression and attribute. // e.g. `userName eq "john"` will return a string comparator that checks whether the passed value is equal to "john". -func createCompareFunction(e *filter.AttributeExpression, attr schema.CoreAttribute) (func(interface{}) error, error) { +func createCompareFunction(e *filter.AttributeExpression, attr schema.CoreAttribute) (func(any) error, error) { switch typ := attr.AttributeType(); typ { case "binary": ref, ok := e.CompareValue.(string) diff --git a/filter/operators_test.go b/filter/operators_test.go index 4c2b99f..3c13bab 100644 --- a/filter/operators_test.go +++ b/filter/operators_test.go @@ -12,14 +12,14 @@ func TestValidatorInvalidResourceTypes(t *testing.T) { name string filter string attr schema.CoreAttribute - resource map[string]interface{} + resource map[string]any }{ { "string", `attr eq "value"`, schema.SimpleCoreAttribute(schema.SimpleStringParams(schema.StringParams{ Name: "attr", })), - map[string]interface{}{ + map[string]any{ "attr": 1, // expects a string }, }, @@ -29,8 +29,8 @@ func TestValidatorInvalidResourceTypes(t *testing.T) { Name: "attr", MultiValued: true, })), - map[string]interface{}{ - "attr": []interface{}{1}, // expects a []interface{string} + map[string]any{ + "attr": []any{1}, // expects a []interface{string} }, }, { @@ -40,7 +40,7 @@ func TestValidatorInvalidResourceTypes(t *testing.T) { Name: "attr", MultiValued: true, })), - map[string]interface{}{ + map[string]any{ "attr": []string{"value"}, // expects a []interface{} }, }, @@ -49,7 +49,7 @@ func TestValidatorInvalidResourceTypes(t *testing.T) { schema.SimpleCoreAttribute(schema.SimpleDateTimeParams(schema.DateTimeParams{ Name: "attr", })), - map[string]interface{}{ + map[string]any{ "attr": 1, // expects a string }, }, @@ -58,7 +58,7 @@ func TestValidatorInvalidResourceTypes(t *testing.T) { schema.SimpleCoreAttribute(schema.SimpleDateTimeParams(schema.DateTimeParams{ Name: "attr", })), - map[string]interface{}{ + map[string]any{ "attr": "2006-01-02T", // expects a valid dateTime }, }, @@ -67,7 +67,7 @@ func TestValidatorInvalidResourceTypes(t *testing.T) { schema.SimpleCoreAttribute(schema.SimpleBooleanParams(schema.BooleanParams{ Name: "attr", })), - map[string]interface{}{ + map[string]any{ "attr": 1, // expects a boolean }, }, @@ -77,7 +77,7 @@ func TestValidatorInvalidResourceTypes(t *testing.T) { Name: "attr", Type: schema.AttributeTypeDecimal(), })), - map[string]interface{}{ + map[string]any{ "attr": "0", // expects a decimal value }, }, @@ -87,7 +87,7 @@ func TestValidatorInvalidResourceTypes(t *testing.T) { Name: "attr", Type: schema.AttributeTypeInteger(), })), - map[string]interface{}{ + map[string]any{ "attr": 0.0, // expects an integer }, }, diff --git a/filter_test.go b/filter_test.go index 3377adc..51fc268 100644 --- a/filter_test.go +++ b/filter_test.go @@ -25,7 +25,6 @@ func Test_Group_Filter(t *testing.T) { {name: "Happy path with plus sign", filter: "displayName eq \"testGroup+test\"", expectedDisplayName: "testGroup+test"}, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/Groups?filter="+url.QueryEscape(tt.filter), nil) w := httptest.NewRecorder() @@ -40,12 +39,12 @@ func Test_Group_Filter(t *testing.T) { t.Fatal(w.Result().StatusCode, string(bytes)) } - var result map[string]interface{} + var result map[string]any if err := json.Unmarshal(bytes, &result); err != nil { t.Fatal(err) } - resources, ok := result["Resources"].([]interface{}) + resources, ok := result["Resources"].([]any) if !ok { t.Fatal("Resources is not the right type or missing") } @@ -54,7 +53,7 @@ func Test_Group_Filter(t *testing.T) { t.Fatal("one Resource expected") } - firstResource, ok := resources[0].(map[string]interface{}) + firstResource, ok := resources[0].(map[string]any) if !ok { t.Fatal("first Resource is not the right type or missing") } @@ -83,7 +82,6 @@ func Test_User_Filter(t *testing.T) { {name: "Happy path with plus sign", filter: "userName eq \"testUser+test\"", expectedUserName: "testUser+test"}, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/Users?filter="+url.QueryEscape(tt.filter), nil) w := httptest.NewRecorder() @@ -98,12 +96,12 @@ func Test_User_Filter(t *testing.T) { t.Fatal(w.Result().StatusCode, string(bytes)) } - var result map[string]interface{} + var result map[string]any if err := json.Unmarshal(bytes, &result); err != nil { t.Fatal(err) } - resources, ok := result["Resources"].([]interface{}) + resources, ok := result["Resources"].([]any) if !ok { t.Fatal("Resources is not the right type or missing") } @@ -112,7 +110,7 @@ func Test_User_Filter(t *testing.T) { t.Fatal("one Resource expected") } - firstResource, ok := resources[0].(map[string]interface{}) + firstResource, ok := resources[0].(map[string]any) if !ok { t.Fatal("first Resource is not the right type or missing") } @@ -144,8 +142,8 @@ func newTestServerForFilter(t *testing.T) scim.Server { Schema: schema.CoreUserSchema(), Handler: &testResourceHandler{ data: map[string]testData{ - "0001": {attributes: map[string]interface{}{"userName": "testUser"}}, - "0002": {attributes: map[string]interface{}{"userName": "testUser+test"}}, + "0001": {attributes: map[string]any{"userName": "testUser"}}, + "0002": {attributes: map[string]any{"userName": "testUser+test"}}, }, schema: schema.CoreUserSchema(), }, @@ -158,8 +156,8 @@ func newTestServerForFilter(t *testing.T) scim.Server { Schema: schema.CoreGroupSchema(), Handler: &testResourceHandler{ data: map[string]testData{ - "0001": {attributes: map[string]interface{}{"displayName": "testGroup"}}, - "0002": {attributes: map[string]interface{}{"displayName": "testGroup+test"}}, + "0001": {attributes: map[string]any{"displayName": "testGroup"}}, + "0002": {attributes: map[string]any{"displayName": "testGroup+test"}}, }, schema: schema.CoreGroupSchema(), }, diff --git a/go.mod b/go.mod index 1a1fcb3..319fee9 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,10 @@ module github.com/elimity-com/scim -go 1.16 +go 1.26.1 require ( github.com/di-wu/xsd-datetime v1.0.0 github.com/scim2/filter-parser/v2 v2.2.0 ) + +require github.com/di-wu/parser v0.3.0 // indirect diff --git a/go.sum b/go.sum index 458c7fa..6e39c74 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ -github.com/di-wu/parser v0.2.2 h1:I9oHJ8spBXOeL7Wps0ffkFFFiXJf/pk7NX9lcAMqRMU= github.com/di-wu/parser v0.2.2/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo= +github.com/di-wu/parser v0.3.0 h1:NMOvy5ifswgt4gsdhySVcKOQtvjC43cHZIfViWctqQY= +github.com/di-wu/parser v0.3.0/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo= github.com/di-wu/xsd-datetime v1.0.0 h1:vZoGNkbzpBNoc+JyfVLEbutNDNydYV8XwHeV7eUJoxI= github.com/di-wu/xsd-datetime v1.0.0/go.mod h1:i3iEhrP3WchwseOBeIdW/zxeoleXTOzx1WyDXgdmOww= github.com/scim2/filter-parser/v2 v2.2.0 h1:QGadEcsmypxg8gYChRSM2j1edLyE/2j72j+hdmI4BJM= diff --git a/handlers.go b/handlers.go index 7f207ee..9ec19ee 100644 --- a/handlers.go +++ b/handlers.go @@ -256,7 +256,7 @@ func (s Server) resourceTypesHandler(w http.ResponseWriter, r *http.Request) { } start, end := clamp(params.StartIndex-1, params.Count, len(s.resourceTypes)) - var resources []interface{} + var resources []any for _, v := range s.resourceTypes[start:end] { resources = append(resources, v.getRaw()) } @@ -369,7 +369,7 @@ func (s Server) schemasHandler(w http.ResponseWriter, r *http.Request) { var ( start, end = clamp(params.StartIndex-1, params.Count, len(s.getSchemas())) - resources []interface{} + resources []any ) if validator := params.FilterValidator; validator != nil { if err := validator.Validate(); err != nil { diff --git a/handlers_test.go b/handlers_test.go index 9d7e6c2..c166abc 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -164,13 +164,13 @@ func TestServerResourceGetHandler(t *testing.T) { assertEqual(t, tt.expectedVersion, rr.Header().Get("Etag")) - var resource map[string]interface{} + var resource map[string]any assertUnmarshalNoError(t, json.Unmarshal(rr.Body.Bytes(), &resource)) assertEqual(t, tt.expectedUserName, resource["userName"]) assertEqual(t, tt.expectedExternalID, resource["externalId"]) - meta, ok := resource["meta"].(map[string]interface{}) + meta, ok := resource["meta"].(map[string]any) assertTypeOk(t, ok, "object") assertEqual(t, "User", meta["resourceType"]) @@ -212,7 +212,7 @@ func TestServerResourcePatchHandlerFailOnBadType(t *testing.T) { rr := httptest.NewRecorder() newTestServer(t).ServeHTTP(rr, req) - var resource map[string]interface{} + var resource map[string]any assertUnmarshalNoError(t, json.Unmarshal(rr.Body.Bytes(), &resource)) assertEqualStatusCode(t, http.StatusBadRequest, rr.Code) @@ -368,7 +368,7 @@ func TestServerResourcePatchHandlerValid(t *testing.T) { assertEqual(t, expectedVersion, rr.Header().Get("Etag")) - var resource map[string]interface{} + var resource map[string]any assertUnmarshalNoError(t, json.Unmarshal(rr.Body.Bytes(), &resource)) assertEqualStatusCode(t, http.StatusOK, rr.Code) @@ -377,11 +377,11 @@ func TestServerResourcePatchHandlerValid(t *testing.T) { assertFalse(t, resource["active"].(bool)) assertEqual(t, "external_test_replace", resource["externalId"]) - if resource["emails"] == nil || len(resource["emails"].([]interface{})) < 1 { + if resource["emails"] == nil || len(resource["emails"].([]any)) < 1 { t.Errorf("handler did not add user's email address") } - meta, ok := resource["meta"].(map[string]interface{}) + meta, ok := resource["meta"].(map[string]any) assertTrue(t, ok) assertEqual(t, "User", meta["resourceType"]) @@ -435,7 +435,7 @@ func TestServerResourcePostHandlerValid(t *testing.T) { target string body io.Reader expectedUserName string - expectedExternalID interface{} + expectedExternalID any }{ { name: "Users post request without version", @@ -474,14 +474,14 @@ func TestServerResourcePostHandlerValid(t *testing.T) { assertEqual(t, "application/scim+json", rr.Header().Get("Content-Type")) - var resource map[string]interface{} + var resource map[string]any assertUnmarshalNoError(t, json.Unmarshal(rr.Body.Bytes(), &resource)) assertEqual(t, test.expectedUserName, resource["userName"]) assertEqual(t, test.expectedExternalID, resource["externalId"]) - meta, ok := resource["meta"].(map[string]interface{}) + meta, ok := resource["meta"].(map[string]any) assertTypeOk(t, ok, "object") assertEqual(t, "User", meta["resourceType"]) @@ -522,7 +522,7 @@ func TestServerResourcePutHandlerValid(t *testing.T) { target string body io.Reader expectedUserName string - expectedExternalID interface{} + expectedExternalID any }{ { name: "Users put request", @@ -555,13 +555,13 @@ func TestServerResourcePutHandlerValid(t *testing.T) { assertEqual(t, "application/scim+json", rr.Header().Get("Content-Type")) - var resource map[string]interface{} + var resource map[string]any assertUnmarshalNoError(t, json.Unmarshal(rr.Body.Bytes(), &resource)) assertEqual(t, test.expectedUserName, resource["userName"]) assertEqual(t, test.expectedExternalID, resource["externalId"]) - meta, ok := resource["meta"].(map[string]interface{}) + meta, ok := resource["meta"].(map[string]any) assertTypeOk(t, ok, "meta") assertEqual(t, "User", meta["resourceType"]) }) @@ -599,7 +599,7 @@ func TestServerResourceTypeHandlerValid(t *testing.T) { assertEqualStatusCode(t, http.StatusOK, rr.Code) - var resourceType map[string]interface{} + var resourceType map[string]any assertUnmarshalNoError(t, json.Unmarshal(rr.Body.Bytes(), &resourceType)) assertEqual(t, tt.resourceType, resourceType["id"]) @@ -637,7 +637,7 @@ func TestServerResourceTypesHandler(t *testing.T) { resourceTypes := make([]string, 3) for i, resource := range response.Resources { - resourceType, ok := resource.(map[string]interface{}) + resourceType, ok := resource.(map[string]any) assertTypeOk(t, ok, "object") resourceTypes[i] = resourceType["name"].(string) } @@ -756,7 +756,7 @@ func TestServerSchemaEndpointValid(t *testing.T) { assertEqualStatusCode(t, http.StatusOK, rr.Code) - var s map[string]interface{} + var s map[string]any assertUnmarshalNoError(t, json.Unmarshal(rr.Body.Bytes(), &s)) assertEqual(t, test.schema, s["id"].(string)) }) @@ -794,7 +794,7 @@ func TestServerSchemasEndpoint(t *testing.T) { resourceIDs := make([]string, 3) for i, resource := range response.Resources { - resourceType, ok := resource.(map[string]interface{}) + resourceType, ok := resource.(map[string]any) assertTypeOk(t, ok, "object") resourceIDs[i] = resourceType["id"].(string) } diff --git a/internal/idp_test/azuread_util_test.go b/internal/idp_test/azuread_util_test.go index e12863b..614f683 100644 --- a/internal/idp_test/azuread_util_test.go +++ b/internal/idp_test/azuread_util_test.go @@ -155,14 +155,14 @@ func (a azureADUserResourceHandler) Get(r *http.Request, id string) (scim.Resour ExternalID: optional.NewString("58342554-38d6-4ec8-948c-50044d0a33fd"), Attributes: scim.ResourceAttributes{ "userName": "Test_User_feed3ace-693c-4e5a-82e2-694be1b39934", - "name": map[string]interface{}{ + "name": map[string]any{ "formatted": "givenName familyName", "familyName": "familyName", "givenName": "givenName", }, "active": true, - "emails": []interface{}{ - map[string]interface{}{ + "emails": []any{ + map[string]any{ "value": "Test_User_22370c1a-9012-42b2-bf64-86099c2a1c22@testuser.com", "type": "work", "primary": true, @@ -190,13 +190,13 @@ func (a azureADUserResourceHandler) GetAll(r *http.Request, params scim.ListRequ ExternalID: optional.NewString("7fce0092-d52e-4f76-b727-3955bd72c939"), Attributes: scim.ResourceAttributes{ "userName": "Test_User_dfeef4c5-5681-4387-b016-bdf221e82081", - "name": map[string]interface{}{ + "name": map[string]any{ "familyName": "familyName", "givenName": "givenName", }, "active": true, - "emails": []interface{}{ - map[string]interface{}{ + "emails": []any{ + map[string]any{ "value": "Test_User_91b67701-697b-46de-b864-bd0bbe4f99c1@testuser.com", "type": "work", "primary": true, @@ -218,14 +218,14 @@ func (a azureADUserResourceHandler) Patch(r *http.Request, id string, operations ExternalID: optional.NewString("6c75de36-30fa-4d2d-a196-6bdcdb6b6539"), Attributes: scim.ResourceAttributes{ "userName": "5b50642d-79fc-4410-9e90-4c077cdd1a59@testuser.com", - "name": map[string]interface{}{ + "name": map[string]any{ "formatted": "givenName updatedFamilyName", "familyName": "updatedFamilyName", "givenName": "givenName", }, "active": false, - "emails": []interface{}{ - map[string]interface{}{ + "emails": []any{ + map[string]any{ "value": "updatedEmail@microsoft.com", "type": "work", "primary": true, diff --git a/internal/idp_test/idp_test.go b/internal/idp_test/idp_test.go index f093ec1..266ae2c 100644 --- a/internal/idp_test/idp_test.go +++ b/internal/idp_test/idp_test.go @@ -47,7 +47,7 @@ func testRequest(t *testing.T, tc testCase, idpName string) error { return fmt.Errorf("expected %d, got %d", tc.StatusCode, code) } if len(tc.Response) != 0 { - var response map[string]interface{} + var response map[string]any if err := unmarshal(rr.Body.Bytes(), &response); err != nil { return err } @@ -60,7 +60,7 @@ func testRequest(t *testing.T, tc testCase, idpName string) error { type testCase struct { Request json.RawMessage - Response map[string]interface{} + Response map[string]any Method string Path string StatusCode int diff --git a/internal/idp_test/okta_util_test.go b/internal/idp_test/okta_util_test.go index 066edc5..6da4f75 100644 --- a/internal/idp_test/okta_util_test.go +++ b/internal/idp_test/okta_util_test.go @@ -62,8 +62,8 @@ func (o oktaGroupResourceHandler) Get(r *http.Request, id string) (scim.Resource ID: id, Attributes: scim.ResourceAttributes{ "displayName": "Test SCIMv2", - "members": []interface{}{ - map[string]interface{}{ + "members": []any{ + map[string]any{ "value": "b1c794f24f4c49f4b5d503a4cb2686ea", "display": "SCIM 2 Group A", }, @@ -115,13 +115,13 @@ func (t oktaUserResourceHandler) Get(r *http.Request, id string) (scim.Resource, ID: id, Attributes: scim.ResourceAttributes{ "userName": "test.user@okta.local", - "name": map[string]interface{}{ + "name": map[string]any{ "givenName": "Test", "familyName": "User", }, "active": true, - "emails": []interface{}{ - map[string]interface{}{ + "emails": []any{ + map[string]any{ "primary": true, "value": "test.user@okta.local", "type": "work", @@ -141,13 +141,13 @@ func (t oktaUserResourceHandler) Patch(r *http.Request, id string, operations [] ID: id, Attributes: scim.ResourceAttributes{ "userName": "test.user@okta.local", - "name": map[string]interface{}{ + "name": map[string]any{ "givenName": "Another", "familyName": "User", }, "active": false, - "emails": []interface{}{ - map[string]interface{}{ + "emails": []any{ + map[string]any{ "primary": true, "value": "test.user@okta.local", "type": "work", diff --git a/internal/idp_test/util_test.go b/internal/idp_test/util_test.go index ae9b1fa..f42a52d 100644 --- a/internal/idp_test/util_test.go +++ b/internal/idp_test/util_test.go @@ -19,7 +19,7 @@ func getNewServer(t *testing.T, idpName string) scim.Server { } } -func unmarshal(data []byte, v interface{}) error { +func unmarshal(data []byte, v any) error { d := json.NewDecoder(bytes.NewReader(data)) d.UseNumber() return d.Decode(v) diff --git a/internal/patch/add_test.go b/internal/patch/add_test.go index fddd1b8..8eee13f 100644 --- a/internal/patch/add_test.go +++ b/internal/patch/add_test.go @@ -8,10 +8,10 @@ import ( // The following example shows how to add a member to a group. func Example_addMemberToGroup() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "add", "path": "members", - "value": map[string]interface{}{ + "value": map[string]any{ "display": "di-wu", "$ref": "https://example.com/v2/Users/0001", "value": "0001", @@ -25,10 +25,10 @@ func Example_addMemberToGroup() { // The following example shows how to add one or more attributes to a User resource without using a "path" attribute. func Example_addWithoutPath() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "add", - "value": map[string]interface{}{ - "emails": []map[string]interface{}{ + "value": map[string]any{ + "emails": []map[string]any{ { "value": "quint@elimity.com", "type": "work", diff --git a/internal/patch/patch.go b/internal/patch/patch.go index 70beef0..d01a435 100644 --- a/internal/patch/patch.go +++ b/internal/patch/patch.go @@ -28,7 +28,7 @@ const ( type OperationValidator struct { Op Op Path *filter.Path - value interface{} + value any schema schema.Schema schemas map[string]schema.Schema @@ -40,7 +40,7 @@ func NewValidator(patchReq []byte, s schema.Schema, extensions ...schema.Schema) var operation struct { Op string Path string - Value interface{} + Value any } d := json.NewDecoder(bytes.NewReader(patchReq)) @@ -55,7 +55,7 @@ func NewValidator(patchReq []byte, s schema.Schema, extensions ...schema.Schema) // Okta also send the ID on PATCH requests. // See: internal/idp_test/testdata/okta/update_group_name.json // https://developer.okta.com/docs/reference/scim/scim-20/#update-a-specific-group-name - case map[string]interface{}: + case map[string]any: var key string var found bool for k := range v { @@ -102,7 +102,7 @@ func NewValidator(patchReq []byte, s schema.Schema, extensions ...schema.Schema) // Validate validates the PATCH operation. Unknown attributes in complex values are ignored. The returned interface // contains a (sanitised) version of given value based on the attribute it targets. Multi-valued attributes will always // be returned wrapped in a slice, even if it is just one value that was defined within the operation. -func (v OperationValidator) Validate() (interface{}, error) { +func (v OperationValidator) Validate() (any, error) { switch v.Op { case OperationAdd, OperationReplace: return v.validateUpdate() @@ -175,13 +175,13 @@ func (v OperationValidator) getRefSubAttribute(refAttr *schema.CoreAttribute, su // validateEmptyPath validates paths that don't have a "path" value. In this case the target location is assumed to be // the resource itself. The "value" parameter contains a set of attributes to be added to the resource. -func (v OperationValidator) validateEmptyPath() (interface{}, error) { - attributes, ok := v.value.(map[string]interface{}) +func (v OperationValidator) validateEmptyPath() (any, error) { + attributes, ok := v.value.(map[string]any) if !ok { return nil, fmt.Errorf("the given value should be a complex attribute if path is empty") } - rootValue := map[string]interface{}{} + rootValue := map[string]any{} for p, value := range attributes { path, err := filter.ParsePath([]byte(p)) if err != nil { diff --git a/internal/patch/patch_test.go b/internal/patch/patch_test.go index 6f0de67..9f568af 100644 --- a/internal/patch/patch_test.go +++ b/internal/patch/patch_test.go @@ -9,7 +9,7 @@ import ( func TestNewPathValidator(t *testing.T) { t.Run("Valid Integer", func(t *testing.T) { - for _, op := range []map[string]interface{}{ + for _, op := range []map[string]any{ {"op": "add", "path": "attr2", "value": 1234}, {"op": "add", "path": "attr2", "value": "1234"}, } { @@ -35,7 +35,7 @@ func TestNewPathValidator(t *testing.T) { }) t.Run("Valid Float", func(t *testing.T) { - for _, op := range []map[string]interface{}{ + for _, op := range []map[string]any{ {"op": "add", "path": "attr3", "value": 12.34}, {"op": "add", "path": "attr3", "value": "12.34"}, } { @@ -62,13 +62,13 @@ func TestNewPathValidator(t *testing.T) { t.Run("Valid Booleans", func(t *testing.T) { tests := []struct { - op map[string]interface{} + op map[string]any expected bool }{ - {map[string]interface{}{"op": "add", "path": "attr4", "value": true}, true}, - {map[string]interface{}{"op": "add", "path": "attr4", "value": "True"}, true}, - {map[string]interface{}{"op": "add", "path": "attr4", "value": false}, false}, - {map[string]interface{}{"op": "add", "path": "attr4", "value": "False"}, false}, + {map[string]any{"op": "add", "path": "attr4", "value": true}, true}, + {map[string]any{"op": "add", "path": "attr4", "value": "True"}, true}, + {map[string]any{"op": "add", "path": "attr4", "value": false}, false}, + {map[string]any{"op": "add", "path": "attr4", "value": "False"}, false}, } for _, tc := range tests { operation, _ := json.Marshal(tc.op) @@ -91,9 +91,82 @@ func TestNewPathValidator(t *testing.T) { } } }) + t.Run("Complex attribute with string value (AllowStringValues)", func(t *testing.T) { + schema.SetAllowStringValues(true) + defer schema.SetAllowStringValues(false) + + op, _ := json.Marshal(map[string]any{ + "op": "add", + "path": "complexWithValue", + "value": "manager-id-123", + }) + validator, err := NewValidator(op, patchSchema) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + v, err := validator.Validate() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + m, ok := v.(map[string]any) + if !ok { + t.Fatalf("expected map[string]any, got %T", v) + } + if m["value"] != "manager-id-123" { + t.Errorf("expected value %q, got %q", "manager-id-123", m["value"]) + } + }) + t.Run("Complex attribute with string value rejected without AllowStringValues", func(t *testing.T) { + schema.SetAllowStringValues(false) + + op, _ := json.Marshal(map[string]any{ + "op": "add", + "path": "complexWithValue", + "value": "manager-id-123", + }) + validator, err := NewValidator(op, patchSchema) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + _, err = validator.Validate() + if err == nil { + t.Fatal("expected error for string value on complex attribute, got none") + } + }) + t.Run("Complex attribute with map value still works", func(t *testing.T) { + schema.SetAllowStringValues(true) + defer schema.SetAllowStringValues(false) + + op, _ := json.Marshal(map[string]any{ + "op": "add", + "path": "complexWithValue", + "value": map[string]any{ + "value": "manager-id-123", + "displayName": "Test Manager", + }, + }) + validator, err := NewValidator(op, patchSchema) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + v, err := validator.Validate() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + m, ok := v.(map[string]any) + if !ok { + t.Fatalf("expected map[string]any, got %T", v) + } + if m["value"] != "manager-id-123" { + t.Errorf("expected value %q, got %q", "manager-id-123", m["value"]) + } + if m["displayName"] != "Test Manager" { + t.Errorf("expected displayName %q, got %q", "Test Manager", m["displayName"]) + } + }) t.Run("Invalid Op", func(t *testing.T) { // "op" must be one of "add", "remove", or "replace". - op, _ := json.Marshal(map[string]interface{}{ + op, _ := json.Marshal(map[string]any{ "op": "invalid", "path": "attr1", "value": "value", @@ -106,7 +179,7 @@ func TestNewPathValidator(t *testing.T) { t.Run("Invalid Attribute", func(t *testing.T) { // "invalid pr" is not a valid path filter. // This error will be caught by the path filter validator. - op, _ := json.Marshal(map[string]interface{}{ + op, _ := json.Marshal(map[string]any{ "op": "add", "path": "invalid pr", "value": "value", @@ -126,7 +199,7 @@ func TestOperationValidator_getRefAttribute(t *testing.T) { {`name.givenName`, `givenName`}, {`urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber`, `employeeNumber`}, } { - op, _ := json.Marshal(map[string]interface{}{ + op, _ := json.Marshal(map[string]any{ "op": "add", "path": test.pathFilter, "value": "value", @@ -148,7 +221,7 @@ func TestOperationValidator_getRefAttribute(t *testing.T) { } } - op, _ := json.Marshal(map[string]interface{}{ + op, _ := json.Marshal(map[string]any{ "op": "invalid", "path": "complex", "value": "value", @@ -173,7 +246,7 @@ func TestOperationValidator_getRefSubAttribute(t *testing.T) { {`name`, `givenName`}, {`groups`, `display`}, } { - op, _ := json.Marshal(map[string]interface{}{ + op, _ := json.Marshal(map[string]any{ "op": "invalid", "path": test.attributeName, "value": "value", diff --git a/internal/patch/remove.go b/internal/patch/remove.go index ebd88fe..1499682 100644 --- a/internal/patch/remove.go +++ b/internal/patch/remove.go @@ -10,7 +10,7 @@ import ( // validateRemove validates the remove operation contained within the validator based on on Section 3.5.2.2 in RFC 7644. // More info: https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2.2 -func (v OperationValidator) validateRemove() (interface{}, error) { +func (v OperationValidator) validateRemove() (any, error) { // If "path" is unspecified, the operation fails with HTTP status code 400 and a "scimType" error code of "noTarget". if v.Path == nil { return nil, &errors.ScimError{ @@ -48,8 +48,8 @@ func (v OperationValidator) validateRemove() (interface{}, error) { return attr, nil } - if list, ok := v.value.([]interface{}); ok { - var attrs []interface{} + if list, ok := v.value.([]any); ok { + var attrs []any for _, value := range list { attr, scimErr := refAttr.ValidateSingular(value) if scimErr != nil { @@ -64,5 +64,5 @@ func (v OperationValidator) validateRemove() (interface{}, error) { if scimErr != nil { return nil, scimErr } - return []interface{}{attr}, nil + return []any{attr}, nil } diff --git a/internal/patch/remove_test.go b/internal/patch/remove_test.go index f382ac2..464f52e 100644 --- a/internal/patch/remove_test.go +++ b/internal/patch/remove_test.go @@ -10,7 +10,7 @@ import ( // The following example shows how remove all members of a group. func Example_removeAllMembers() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "remove", "path": "members", }) @@ -22,7 +22,7 @@ func Example_removeAllMembers() { // The following example shows how remove a value from a complex multi-valued attribute. func Example_removeComplexMultiValuedAttributeValue() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "remove", "path": `emails[type eq "work" and value eq "elimity.com"]`, }) @@ -34,11 +34,11 @@ func Example_removeComplexMultiValuedAttributeValue() { // The following example shows how remove a single group from a user. func Example_removeSingleGroup() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "remove", "path": "groups", - "value": []interface{}{ - map[string]interface{}{ + "value": []any{ + map[string]any{ "$ref": nil, "value": "f648f8d5ea4e4cd38e9c", }, @@ -52,7 +52,7 @@ func Example_removeSingleGroup() { // The following example shows how remove a single member from a group. func Example_removeSingleMember() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "remove", "path": `members[value eq "0001"]`, }) @@ -64,7 +64,7 @@ func Example_removeSingleMember() { // The following example shows how to replace all of the members of a group with a different members list. func Example_replaceAllMembers() { - operations := []map[string]interface{}{ + operations := []map[string]any{ { "op": "remove", "path": "members", @@ -72,8 +72,8 @@ func Example_replaceAllMembers() { { "op": "remove", "path": "members", - "value": []interface{}{ - map[string]interface{}{ + "value": []any{ + map[string]any{ "value": "f648f8d5ea4e4cd38e9c", }, }, @@ -81,13 +81,13 @@ func Example_replaceAllMembers() { { "op": "add", "path": "members", - "value": []interface{}{ - map[string]interface{}{ + "value": []any{ + map[string]any{ "display": "di-wu", "$ref": "https://example.com/v2/Users/0001", "value": "0001", }, - map[string]interface{}{ + map[string]any{ "display": "example", "$ref": "https://example.com/v2/Users/0002", "value": "0002", @@ -120,22 +120,22 @@ func TestOperationValidator_ValidateRemove(t *testing.T) { // attribute's sub-attributes, the matching records are removed. for i, test := range []struct { - valid map[string]interface{} - invalid map[string]interface{} + valid map[string]any + invalid map[string]any }{ // If "path" is unspecified, the operation fails. - {invalid: map[string]interface{}{"op": "remove"}}, + {invalid: map[string]any{"op": "remove"}}, // If the target location is a single-value attribute. - {valid: map[string]interface{}{"op": "remove", "path": "attr1"}}, + {valid: map[string]any{"op": "remove", "path": "attr1"}}, // If the target location is a multi-valued attribute and no filter is specified. - {valid: map[string]interface{}{"op": "remove", "path": "multiValued"}}, + {valid: map[string]any{"op": "remove", "path": "multiValued"}}, // If the target location is a multi-valued attribute and a complex filter is specified comparing a "value". - {valid: map[string]interface{}{"op": "remove", "path": `multivalued[value eq "value"]`}}, + {valid: map[string]any{"op": "remove", "path": `multivalued[value eq "value"]`}}, // If the target location is a complex multi-valued attribute and a complex filter is specified based on the // attribute's sub-attributes - {valid: map[string]interface{}{"op": "remove", "path": `complexMultiValued[attr1 eq "value"]`}}, - {valid: map[string]interface{}{"op": "remove", "path": `complexMultiValued[attr1 eq "value"].attr1`}}, + {valid: map[string]any{"op": "remove", "path": `complexMultiValued[attr1 eq "value"]`}}, + {valid: map[string]any{"op": "remove", "path": `complexMultiValued[attr1 eq "value"].attr1`}}, } { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { // valid diff --git a/internal/patch/replace_test.go b/internal/patch/replace_test.go index 34347ec..dbf7520 100644 --- a/internal/patch/replace_test.go +++ b/internal/patch/replace_test.go @@ -8,10 +8,10 @@ import ( // The following example shows how to replace all values of one or more specific attributes. func Example_replaceAnyAttribute() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "replace", - "value": map[string]interface{}{ - "emails": []map[string]interface{}{ + "value": map[string]any{ + "emails": []map[string]any{ { "value": "quint", "type": "work", @@ -34,17 +34,17 @@ func Example_replaceAnyAttribute() { // The following example shows how to replace all of the members of a group with a different members list in a single // replace operation. func Example_replaceMembers() { - operations := []map[string]interface{}{ + operations := []map[string]any{ { "op": "replace", "path": "members", - "value": []interface{}{ - map[string]interface{}{ + "value": []any{ + map[string]any{ "display": "di-wu", "$ref": "https://example.com/v2/Users/0001", "value": "0001", }, - map[string]interface{}{ + map[string]any{ "display": "example", "$ref": "https://example.com/v2/Users/0002", "value": "0002", @@ -64,7 +64,7 @@ func Example_replaceMembers() { // The following example shows how to change a specific sub-attribute "streetAddress" of complex attribute "emails" // selected by a "valuePath" filter. func Example_replaceSpecificSubAttribute() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "replace", "path": `addresses[type eq "work"].streetAddress`, "value": "ExampleStreet 100", @@ -77,10 +77,10 @@ func Example_replaceSpecificSubAttribute() { // The following example shows how to change a User's entire "work" address, using a "valuePath" filter. func Example_replaceWorkAddress() { - operation, _ := json.Marshal(map[string]interface{}{ + operation, _ := json.Marshal(map[string]any{ "op": "replace", "path": `addresses[type eq "work"]`, - "value": map[string]interface{}{ + "value": map[string]any{ "type": "work", "streetAddress": "ExampleStreet 1", "locality": "ExampleCity", diff --git a/internal/patch/schema_test.go b/internal/patch/schema_test.go index c7c6c6a..a9bf27c 100644 --- a/internal/patch/schema_test.go +++ b/internal/patch/schema_test.go @@ -40,16 +40,27 @@ var ( }), }, }), - schema.ComplexCoreAttribute(schema.ComplexParams{ - Name: "complexMultiValued", - MultiValued: true, - SubAttributes: []schema.SimpleParams{ - schema.SimpleStringParams(schema.StringParams{ - Name: "attr1", - }), - }, - }), - }, + schema.ComplexCoreAttribute(schema.ComplexParams{ + Name: "complexMultiValued", + MultiValued: true, + SubAttributes: []schema.SimpleParams{ + schema.SimpleStringParams(schema.StringParams{ + Name: "attr1", + }), + }, + }), + schema.ComplexCoreAttribute(schema.ComplexParams{ + Name: "complexWithValue", + SubAttributes: []schema.SimpleParams{ + schema.SimpleStringParams(schema.StringParams{ + Name: "value", + }), + schema.SimpleStringParams(schema.StringParams{ + Name: "displayName", + }), + }, + }), + }, } patchSchemaExtension = schema.Schema{ ID: "test:PatchExtension", diff --git a/internal/patch/update.go b/internal/patch/update.go index 8c0f599..7b18433 100644 --- a/internal/patch/update.go +++ b/internal/patch/update.go @@ -8,7 +8,7 @@ import ( // validateUpdate validates the add/replace operation contained within the validator based on on Section 3.5.2.1 in // RFC 7644. More info: https://datatracker.ietf.org/doc/html/rfc7644#section-3.5.2.1 -func (v OperationValidator) validateUpdate() (interface{}, error) { +func (v OperationValidator) validateUpdate() (any, error) { // The operation must contain a "value" member whose content specifies the value to be added/replaces. if v.value == nil { return nil, fmt.Errorf("an add operation must contain a value member") @@ -46,8 +46,8 @@ func (v OperationValidator) validateUpdate() (interface{}, error) { return attr, nil } - if list, ok := v.value.([]interface{}); ok { - var attrs []interface{} + if list, ok := v.value.([]any); ok { + var attrs []any for _, value := range list { attr, scimErr := refAttr.ValidateSingular(value) if scimErr != nil { @@ -62,5 +62,5 @@ func (v OperationValidator) validateUpdate() (interface{}, error) { if scimErr != nil { return nil, scimErr } - return []interface{}{attr}, nil + return []any{attr}, nil } diff --git a/internal/patch/update_test.go b/internal/patch/update_test.go index 4c18ee3..acaf98c 100644 --- a/internal/patch/update_test.go +++ b/internal/patch/update_test.go @@ -26,21 +26,21 @@ func TestOperationValidator_ValidateUpdate(t *testing.T) { // - Unless other operations change the resource, this operation shall not change the modify timestamp of the // resource. for i, test := range []struct { - valid map[string]interface{} - invalid map[string]interface{} + valid map[string]any + invalid map[string]any }{ // The operation must contain a "value" member whose content specifies the value to be added. { - valid: map[string]interface{}{"op": "add", "path": "attr1", "value": "value"}, - invalid: map[string]interface{}{"op": "add", "path": "attr1"}, + valid: map[string]any{"op": "add", "path": "attr1", "value": "value"}, + invalid: map[string]any{"op": "add", "path": "attr1"}, }, // A URI prefix in the path. { - valid: map[string]interface{}{"op": "add", "path": "test:PatchEntity:attr1", "value": "value"}, - invalid: map[string]interface{}{"op": "add", "path": "invalid:attr1", "value": "value"}, + valid: map[string]any{"op": "add", "path": "test:PatchEntity:attr1", "value": "value"}, + invalid: map[string]any{"op": "add", "path": "invalid:attr1", "value": "value"}, }, - {valid: map[string]interface{}{"op": "add", "path": "test:PatchExtension:attr1", "value": "value"}}, + {valid: map[string]any{"op": "add", "path": "test:PatchExtension:attr1", "value": "value"}}, // The value MAY be a quoted value, or it may be a JSON object containing the sub-attributes of the complex // attribute specified in the operation's "path". @@ -50,60 +50,60 @@ func TestOperationValidator_ValidateUpdate(t *testing.T) { // The idea is that path can be either fine-grained or point to a whole object. // Thus value of "value" depends on what path points to. { - valid: map[string]interface{}{"op": "add", "path": "complex.attr1", "value": "value"}, - invalid: map[string]interface{}{"op": "add", "path": "complex.attr1", "value": map[string]interface{}{"attr1": "value"}}, + valid: map[string]any{"op": "add", "path": "complex.attr1", "value": "value"}, + invalid: map[string]any{"op": "add", "path": "complex.attr1", "value": map[string]any{"attr1": "value"}}, }, { - valid: map[string]interface{}{"op": "add", "path": "complex", "value": map[string]interface{}{"attr1": "value"}}, - invalid: map[string]interface{}{"op": "add", "path": "complex", "value": "value"}, + valid: map[string]any{"op": "add", "path": "complex", "value": map[string]any{"attr1": "value"}}, + invalid: map[string]any{"op": "add", "path": "complex", "value": "value"}, }, // If omitted, the target location is assumed to be the resource itself. The "value" parameter contains a // set of attributes to be added to the resource. { - valid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"attr1": "value"}}, - invalid: map[string]interface{}{"op": "add", "value": "value"}, + valid: map[string]any{"op": "add", "value": map[string]any{"attr1": "value"}}, + invalid: map[string]any{"op": "add", "value": "value"}, }, - {invalid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"invalid": "value"}}}, - {invalid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"invalid:attr1": "value"}}}, + {invalid: map[string]any{"op": "add", "value": map[string]any{"invalid": "value"}}}, + {invalid: map[string]any{"op": "add", "value": map[string]any{"invalid:attr1": "value"}}}, // If the target location specifies a multi-valued attribute, a new value is added to the attribute. - {valid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"multiValued": "value"}}}, + {valid: map[string]any{"op": "add", "value": map[string]any{"multiValued": "value"}}}, // Example on page 36 (RFC7644, Section 3.5.2.1). - {valid: map[string]interface{}{"op": "add", "path": "complexMultiValued", "value": []interface{}{map[string]interface{}{"attr1": "value"}}}}, - {valid: map[string]interface{}{"op": "add", "path": "complexMultiValued", "value": map[string]interface{}{"attr1": "value"}}}, + {valid: map[string]any{"op": "add", "path": "complexMultiValued", "value": []any{map[string]any{"attr1": "value"}}}}, + {valid: map[string]any{"op": "add", "path": "complexMultiValued", "value": map[string]any{"attr1": "value"}}}, // Example on page 37 (RFC7644, Section 3.5.2.1). - {valid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"attr1": "value", "complexMultiValued": []interface{}{map[string]interface{}{"attr1": "value"}}}}}, + {valid: map[string]any{"op": "add", "value": map[string]any{"attr1": "value", "complexMultiValued": []any{map[string]any{"attr1": "value"}}}}}, { - valid: map[string]interface{}{"op": "add", "path": `complexMultiValued[attr1 eq "value"].attr1`, "value": "value"}, - invalid: map[string]interface{}{"op": "add", "path": `complexMultiValued[attr1 eq "value"].attr2`, "value": "value"}, + valid: map[string]any{"op": "add", "path": `complexMultiValued[attr1 eq "value"].attr1`, "value": "value"}, + invalid: map[string]any{"op": "add", "path": `complexMultiValued[attr1 eq "value"].attr2`, "value": "value"}, }, { - valid: map[string]interface{}{"op": "add", "path": `test:PatchEntity:complexMultiValued[attr1 eq "value"].attr1`, "value": "value"}, - invalid: map[string]interface{}{"op": "add", "path": `test:PatchEntity:complexMultiValued[attr2 eq "value"].attr1`, "value": "value"}, + valid: map[string]any{"op": "add", "path": `test:PatchEntity:complexMultiValued[attr1 eq "value"].attr1`, "value": "value"}, + invalid: map[string]any{"op": "add", "path": `test:PatchEntity:complexMultiValued[attr2 eq "value"].attr1`, "value": "value"}, }, // Valid path, attribute not found. - {invalid: map[string]interface{}{"op": "add", "path": "invalid", "value": "value"}}, - {invalid: map[string]interface{}{"op": "add", "path": "complex.invalid", "value": "value"}}, + {invalid: map[string]any{"op": "add", "path": "invalid", "value": "value"}}, + {invalid: map[string]any{"op": "add", "path": "complex.invalid", "value": "value"}}, // Sub-attributes in complex assignments. - {valid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"complex.attr1": "value"}}}, + {valid: map[string]any{"op": "add", "value": map[string]any{"complex.attr1": "value"}}}, // Has no sub-attributes. - {invalid: map[string]interface{}{"op": "add", "path": "attr1.invalid", "value": "value"}}, + {invalid: map[string]any{"op": "add", "path": "attr1.invalid", "value": "value"}}, // Invalid types. - {invalid: map[string]interface{}{"op": "add", "path": "attr1", "value": 1}}, - {invalid: map[string]interface{}{"op": "add", "path": "multiValued", "value": 1}}, - {invalid: map[string]interface{}{"op": "add", "path": "multiValued", "value": []interface{}{1}}}, - {invalid: map[string]interface{}{"op": "add", "path": "complex.attr1", "value": 1}}, - {invalid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"attr1": 1}}}, - {invalid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"multiValued": 1}}}, - {invalid: map[string]interface{}{"op": "add", "value": map[string]interface{}{"multiValued": []interface{}{1}}}}, + {invalid: map[string]any{"op": "add", "path": "attr1", "value": 1}}, + {invalid: map[string]any{"op": "add", "path": "multiValued", "value": 1}}, + {invalid: map[string]any{"op": "add", "path": "multiValued", "value": []any{1}}}, + {invalid: map[string]any{"op": "add", "path": "complex.attr1", "value": 1}}, + {invalid: map[string]any{"op": "add", "value": map[string]any{"attr1": 1}}}, + {invalid: map[string]any{"op": "add", "value": map[string]any{"multiValued": 1}}}, + {invalid: map[string]any{"op": "add", "value": map[string]any{"multiValued": []any{1}}}}, } { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { // valid diff --git a/list_response.go b/list_response.go index 56448f1..d9abf85 100644 --- a/list_response.go +++ b/list_response.go @@ -12,17 +12,17 @@ type Page struct { Resources []Resource } -func (p Page) resources(resourceType ResourceType) []interface{} { +func (p Page) resources(resourceType ResourceType) []any { // If the page.Resources is nil, then it will also be represented as a `null` in the response. // Otherwise is it is an empty slice then it will result in an empty array `[]`. if len(p.Resources) == 0 { if p.Resources != nil { - return []interface{}{} + return []any{} } return nil } - var resources []interface{} + var resources []any for _, v := range p.Resources { resources = append( resources, @@ -51,11 +51,11 @@ type listResponse struct { // Resources is a multi-valued list of complex objects containing the requested resources. // This may be a subset of the full set of resources if pagination is requested. // REQUIRED if TotalResults is non-zero. - Resources []interface{} + Resources []any } func (l listResponse) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ + return json.Marshal(map[string]any{ "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}, "totalResults": l.TotalResults, "itemsPerPage": l.ItemsPerPage, diff --git a/logger.go b/logger.go index 180fe62..81dd125 100644 --- a/logger.go +++ b/logger.go @@ -2,9 +2,9 @@ package scim // Logger defines an interface for logging errors. type Logger interface { - Error(args ...interface{}) + Error(args ...any) } type noopLogger struct{} -func (noopLogger) Error(...interface{}) {} +func (noopLogger) Error(...any) {} diff --git a/patch.go b/patch.go index d82fffa..db2ad90 100644 --- a/patch.go +++ b/patch.go @@ -19,5 +19,5 @@ type PatchOperation struct { // "add" and "replace" and is REQUIRED for "remove" operations. Path *filter.Path // Value specifies the value to be added or replaced. - Value interface{} + Value any } diff --git a/patch_add_test.go b/patch_add_test.go index c7cca2e..9212892 100644 --- a/patch_add_test.go +++ b/patch_add_test.go @@ -22,7 +22,7 @@ func TestPatch_addAttributes(t *testing.T) { if rr.Code != http.StatusOK { t.Fatal(rr.Code, rr.Body.String()) } - var resource map[string]interface{} + var resource map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resource); err != nil { t.Fatal(err) } @@ -30,14 +30,14 @@ func TestPatch_addAttributes(t *testing.T) { if !ok { t.Fatal(resource["emails"]) } - rl, ok := rm.([]interface{}) + rl, ok := rm.([]any) if !ok { t.Fatal(rm) } if len(rl) != 1 { t.Fatal(rl) } - m, ok := rl[0].(map[string]interface{}) + m, ok := rl[0].(map[string]any) if !ok { t.Fatal(rl[0]) } @@ -69,7 +69,7 @@ func TestPatch_addMember(t *testing.T) { if rr.Code != http.StatusOK { t.Fatal(rr.Code, rr.Body.String()) } - var resource map[string]interface{} + var resource map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resource); err != nil { t.Fatal(err) } @@ -77,14 +77,14 @@ func TestPatch_addMember(t *testing.T) { if !ok { t.Fatal(resource["members"]) } - rl, ok := rm.([]interface{}) + rl, ok := rm.([]any) if !ok { t.Fatal(rm) } if len(rl) != 1 { t.Fatal(rl) } - m, ok := rl[0].(map[string]interface{}) + m, ok := rl[0].(map[string]any) if !ok { t.Fatal(rl[0]) } @@ -162,7 +162,7 @@ func TestPatch_complex(t *testing.T) { if rr.Code != http.StatusOK { t.Fatal(rr.Code, rr.Body.String()) } - var resource map[string]interface{} + var resource map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &resource); err != nil { t.Fatal(err) } @@ -170,7 +170,7 @@ func TestPatch_complex(t *testing.T) { if !ok { t.Fatal(resource["members"]) } - m, ok := rm.(map[string]interface{}) + m, ok := rm.(map[string]any) if !ok { t.Fatal(rm) } diff --git a/resource_handler.go b/resource_handler.go index 1d0cd15..c2b6705 100644 --- a/resource_handler.go +++ b/resource_handler.go @@ -88,7 +88,7 @@ func (r Resource) response(resourceType ResourceType) ResourceAttributes { // ResourceAttributes represents a list of attributes given to the callback method to create or replace // a resource based on the given attributes. -type ResourceAttributes map[string]interface{} +type ResourceAttributes map[string]any // ResourceHandler represents a set of callback method that connect the SCIM server with a provider of a certain resource. type ResourceHandler interface { diff --git a/resource_handler_test.go b/resource_handler_test.go index 17e557d..1d93952 100644 --- a/resource_handler_test.go +++ b/resource_handler_test.go @@ -2,6 +2,7 @@ package scim import ( "fmt" + "maps" "math/rand" "net/http" "strings" @@ -12,7 +13,7 @@ import ( ) func ExampleResourceHandler() { - var r interface{} = testResourceHandler{} + var r any = testResourceHandler{} _, ok := r.(ResourceHandler) fmt.Println(ok) // Output: true @@ -131,9 +132,9 @@ func (h testResourceHandler) Patch(r *http.Request, id string, operations []Patc if op.Path != nil { h.data[id].resourceAttributes[op.Path.String()] = op.Value } else { - valueMap := op.Value.(map[string]interface{}) + valueMap := op.Value.(map[string]any) for k, v := range valueMap { - if arr, ok := h.data[id].resourceAttributes[k].([]interface{}); ok { + if arr, ok := h.data[id].resourceAttributes[k].([]any); ok { arr = append(arr, v) h.data[id].resourceAttributes[k] = arr } else { @@ -145,10 +146,8 @@ func (h testResourceHandler) Patch(r *http.Request, id string, operations []Patc if op.Path != nil { h.data[id].resourceAttributes[op.Path.String()] = op.Value } else { - valueMap := op.Value.(map[string]interface{}) - for k, v := range valueMap { - h.data[id].resourceAttributes[k] = v - } + valueMap := op.Value.(map[string]any) + maps.Copy(h.data[id].resourceAttributes, valueMap) } case PatchOperationRemove: h.data[id].resourceAttributes[op.Path.String()] = nil @@ -223,14 +222,14 @@ func (h testResourceHandler) noContentOperation(id string, op PatchOperation) bo } switch opValue := op.Value.(type) { - case map[string]interface{}: + case map[string]any: for k, v := range opValue { if v == dataValue.resourceAttributes[k] { return true } } - case []map[string]interface{}: + case []map[string]any: for _, m := range opValue { for k, v := range m { if v == dataValue.resourceAttributes[k] { diff --git a/resource_type.go b/resource_type.go index 8761d7d..55fd1ed 100644 --- a/resource_type.go +++ b/resource_type.go @@ -12,7 +12,7 @@ import ( ) // unmarshal unifies the unmarshal of the requests. -func unmarshal(data []byte, v interface{}) error { +func unmarshal(data []byte, v any) error { d := json.NewDecoder(bytes.NewReader(data)) d.UseNumber() return d.Decode(v) @@ -38,8 +38,8 @@ type ResourceType struct { Handler ResourceHandler } -func (t ResourceType) getRaw() map[string]interface{} { - return map[string]interface{}{ +func (t ResourceType) getRaw() map[string]any { + return map[string]any{ "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:ResourceType"}, "id": t.ID.Value(), "name": t.Name, @@ -50,10 +50,10 @@ func (t ResourceType) getRaw() map[string]interface{} { } } -func (t ResourceType) getRawSchemaExtensions() []map[string]interface{} { - schemas := make([]map[string]interface{}, 0) +func (t ResourceType) getRawSchemaExtensions() []map[string]any { + schemas := make([]map[string]any, 0) for _, e := range t.SchemaExtensions { - schemas = append(schemas, map[string]interface{}{ + schemas = append(schemas, map[string]any{ "schema": e.Schema.ID, "required": e.Required, }) @@ -87,7 +87,7 @@ func (t ResourceType) schemaWithCommon() schema.Schema { } func (t ResourceType) validate(raw []byte) (ResourceAttributes, *errors.ScimError) { - var m map[string]interface{} + var m map[string]any if err := unmarshal(raw, &m); err != nil { return ResourceAttributes{}, &errors.ScimErrorInvalidSyntax } diff --git a/schema/core.go b/schema/core.go index 37ceaee..f4df859 100644 --- a/schema/core.go +++ b/schema/core.go @@ -170,7 +170,7 @@ func (a CoreAttribute) Uniqueness() string { // ValidateSingular checks whether the given singular value matches the attribute data type. Unknown attributes in // given complex value are ignored. The returned interface contains a (sanitised) version of the given attribute. -func (a CoreAttribute) ValidateSingular(attribute interface{}) (interface{}, *errors.ScimError) { +func (a CoreAttribute) ValidateSingular(attribute any) (any, *errors.ScimError) { switch a.typ { case attributeDataTypeBinary: bin, ok := attribute.(string) @@ -203,15 +203,19 @@ func (a CoreAttribute) ValidateSingular(attribute interface{}) (interface{}, *er return b, nil case attributeDataTypeComplex: - obj, ok := attribute.(map[string]interface{}) + obj, ok := attribute.(map[string]any) if !ok { - return nil, &errors.ScimErrorInvalidValue + if s, ok := attribute.(string); ok && schemaAllowStringValues { + obj = map[string]any{"value": s} + } else { + return nil, &errors.ScimErrorInvalidValue + } } - attributes := make(map[string]interface{}) + attributes := make(map[string]any) for _, sub := range a.subAttributes { - var hit interface{} + var hit any var found bool for k, v := range obj { if strings.EqualFold(sub.name, k) { @@ -291,8 +295,8 @@ func (a CoreAttribute) ValidateSingular(attribute interface{}) (interface{}, *er } } -func (a *CoreAttribute) getRawAttributes() map[string]interface{} { - attributes := map[string]interface{}{ +func (a *CoreAttribute) getRawAttributes() map[string]any { + attributes := map[string]any{ "description": a.description.Value(), "multiValued": a.multiValued, "mutability": a.mutability, @@ -310,7 +314,7 @@ func (a *CoreAttribute) getRawAttributes() map[string]interface{} { attributes["referenceTypes"] = a.referenceTypes } - var rawSubAttributes []map[string]interface{} + var rawSubAttributes []map[string]any for _, subAttr := range a.subAttributes { rawSubAttributes = append(rawSubAttributes, subAttr.getRawAttributes()) } @@ -327,7 +331,7 @@ func (a *CoreAttribute) getRawAttributes() map[string]interface{} { return attributes } -func (a CoreAttribute) validate(attribute interface{}) (interface{}, *errors.ScimError) { +func (a CoreAttribute) validate(attribute any) (any, *errors.ScimError) { // whether or not the attribute is required. if attribute == nil { if !a.required { @@ -349,13 +353,13 @@ func (a CoreAttribute) validate(attribute interface{}) (interface{}, *errors.Sci } switch arr := attribute.(type) { - case map[string]interface{}: + case map[string]any: // return false if the multivalued attribute is empty. if a.required && len(arr) == 0 { return nil, &errors.ScimErrorInvalidValue } - validMap := map[string]interface{}{} + validMap := map[string]any{} for k, v := range arr { for _, sub := range a.subAttributes { if !strings.EqualFold(sub.name, k) { @@ -370,13 +374,13 @@ func (a CoreAttribute) validate(attribute interface{}) (interface{}, *errors.Sci } return validMap, nil - case []interface{}: + case []any: // return false if the multivalued attribute is empty. if a.required && len(arr) == 0 { return nil, &errors.ScimErrorInvalidValue } - var attributes []interface{} + var attributes []any for _, ele := range arr { attr, scimErr := a.ValidateSingular(ele) if scimErr != nil { diff --git a/schema/schema.go b/schema/schema.go index 6556633..c1d9994 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -55,8 +55,8 @@ func (s Schema) MarshalJSON() ([]byte, error) { } // ToMap returns the map representation of a schema. -func (s Schema) ToMap() map[string]interface{} { - return map[string]interface{}{ +func (s Schema) ToMap() map[string]any { + return map[string]any{ "id": s.ID, "name": s.Name.Value(), "description": s.Description.Value(), @@ -67,17 +67,17 @@ func (s Schema) ToMap() map[string]interface{} { // Validate validates given resource based on the schema. Does NOT validate mutability. // NOTE: only used in POST and PUT requests where attributes MAY be (re)defined. -func (s Schema) Validate(resource interface{}) (map[string]interface{}, *errors.ScimError) { +func (s Schema) Validate(resource any) (map[string]any, *errors.ScimError) { return s.validate(resource, false) } // ValidateMutability validates given resource based on the schema, including strict immutability checks. -func (s Schema) ValidateMutability(resource interface{}) (map[string]interface{}, *errors.ScimError) { +func (s Schema) ValidateMutability(resource any) (map[string]any, *errors.ScimError) { return s.validate(resource, true) } // ValidatePatchOperation validates an individual operation and its related value. -func (s Schema) ValidatePatchOperation(operation string, operationValue map[string]interface{}, isExtension bool) *errors.ScimError { +func (s Schema) ValidatePatchOperation(operation string, operationValue map[string]any, isExtension bool) *errors.ScimError { for k, v := range operationValue { var attr *CoreAttribute var scimErr *errors.ScimError @@ -113,12 +113,12 @@ func (s Schema) ValidatePatchOperation(operation string, operationValue map[stri } // ValidatePatchOperationValue validates an individual operation and its related value. -func (s Schema) ValidatePatchOperationValue(operation string, operationValue map[string]interface{}) *errors.ScimError { +func (s Schema) ValidatePatchOperationValue(operation string, operationValue map[string]any) *errors.ScimError { return s.ValidatePatchOperation(operation, operationValue, false) } -func (s Schema) getRawAttributes() []map[string]interface{} { - attributes := make([]map[string]interface{}, len(s.Attributes)) +func (s Schema) getRawAttributes() []map[string]any { + attributes := make([]map[string]any, len(s.Attributes)) for i, a := range s.Attributes { attributes[i] = a.getRawAttributes() @@ -127,15 +127,15 @@ func (s Schema) getRawAttributes() []map[string]interface{} { return attributes } -func (s Schema) validate(resource interface{}, checkMutability bool) (map[string]interface{}, *errors.ScimError) { - core, ok := resource.(map[string]interface{}) +func (s Schema) validate(resource any, checkMutability bool) (map[string]any, *errors.ScimError) { + core, ok := resource.(map[string]any) if !ok { return nil, &errors.ScimErrorInvalidSyntax } - attributes := make(map[string]interface{}) + attributes := make(map[string]any) for _, attribute := range s.Attributes { - var hit interface{} + var hit any var found bool for k, v := range core { if strings.EqualFold(attribute.name, k) { diff --git a/schema/schema_test.go b/schema/schema_test.go index c9faacc..dad67f4 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -98,21 +98,21 @@ func TestJSONMarshalling(t *testing.T) { } func TestResourceInvalid(t *testing.T) { - var resource interface{} + var resource any if _, scimErr := testSchema.Validate(resource); scimErr == nil { t.Error("invalid resource expected") } } func TestValidValidation(t *testing.T) { - for _, test := range []map[string]interface{}{ + for _, test := range []map[string]any{ { "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, - "complex": []interface{}{ - map[string]interface{}{ + "complex": []any{ + map[string]any{ "sub": "present", }, }, @@ -131,59 +131,59 @@ func TestValidValidation(t *testing.T) { } func TestValidationInvalid(t *testing.T) { - for _, test := range []map[string]interface{}{ + for _, test := range []map[string]any{ { // missing required field "field": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, }, { // missing required multivalued field "required": "present", - "booleans": []interface{}{}, + "booleans": []any{}, }, { // wrong type element of slice "required": "present", - "booleans": []interface{}{ + "booleans": []any{ "present", }, }, { // duplicate names "required": "present", "Required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, }, { // wrong string type "required": true, - "booleans": []interface{}{ + "booleans": []any{ true, }, }, { // wrong complex type "required": "present", "complex": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, }, { // wrong complex element type "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, - "complex": []interface{}{ + "complex": []any{ "present", }, }, { // duplicate complex element names "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, - "complex": []interface{}{ - map[string]interface{}{ + "complex": []any{ + map[string]any{ "sub": "present", "Sub": "present", }, @@ -191,53 +191,53 @@ func TestValidationInvalid(t *testing.T) { }, { // wrong type complex element "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, - "complex": []interface{}{ - map[string]interface{}{ + "complex": []any{ + map[string]any{ "sub": true, }, }, }, { // invalid type binary "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, "binary": true, }, { // invalid type dateTime "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, "dateTime": "04:56:22Z2008-01-23T", }, { // invalid type integer "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, "integer": 1.1, }, { // invalid type decimal "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, "decimal": "1.1", }, { // invalid type integer (json.Number) "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, "integerNumber": json.Number("1.1"), }, { // invalid type decimal (json.Number) "required": "present", - "booleans": []interface{}{ + "booleans": []any{ true, }, "decimalNumber": json.Number("fail"), @@ -249,8 +249,70 @@ func TestValidationInvalid(t *testing.T) { } } +func TestValidateSingularComplexWithStringValue(t *testing.T) { + attr := ComplexCoreAttribute(ComplexParams{ + Name: "manager", + SubAttributes: []SimpleParams{ + SimpleStringParams(StringParams{Name: "value"}), + SimpleStringParams(StringParams{Name: "displayName"}), + }, + }) + + t.Run("map value always accepted", func(t *testing.T) { + v, scimErr := attr.ValidateSingular(map[string]any{ + "value": "mgr-123", + "displayName": "Test Manager", + }) + if scimErr != nil { + t.Fatalf("unexpected error: %v", scimErr) + } + m := v.(map[string]any) + if m["value"] != "mgr-123" { + t.Errorf("expected value %q, got %q", "mgr-123", m["value"]) + } + if m["displayName"] != "Test Manager" { + t.Errorf("expected displayName %q, got %q", "Test Manager", m["displayName"]) + } + }) + + t.Run("string value rejected when AllowStringValues is false", func(t *testing.T) { + SetAllowStringValues(false) + _, scimErr := attr.ValidateSingular("mgr-123") + if scimErr == nil { + t.Fatal("expected error, got nil") + } + }) + + t.Run("string value accepted when AllowStringValues is true", func(t *testing.T) { + SetAllowStringValues(true) + defer SetAllowStringValues(false) + + v, scimErr := attr.ValidateSingular("mgr-123") + if scimErr != nil { + t.Fatalf("unexpected error: %v", scimErr) + } + m, ok := v.(map[string]any) + if !ok { + t.Fatalf("expected map[string]any, got %T", v) + } + if m["value"] != "mgr-123" { + t.Errorf("expected value %q, got %q", "mgr-123", m["value"]) + } + }) + + t.Run("non-string non-map value always rejected", func(t *testing.T) { + SetAllowStringValues(true) + defer SetAllowStringValues(false) + + _, scimErr := attr.ValidateSingular(42) + if scimErr == nil { + t.Fatal("expected error for integer value on complex attribute, got nil") + } + }) +} + func normalizeJSON(rawJSON []byte) (string, error) { - dataMap := map[string]interface{}{} + dataMap := map[string]any{} // Ignoring errors since we know it is valid err := json.Unmarshal(rawJSON, &dataMap) diff --git a/server_test.go b/server_test.go index 726fea5..d544d59 100644 --- a/server_test.go +++ b/server_test.go @@ -55,7 +55,7 @@ func newTestServer(t *testing.T) scim.Server { Schema: schema.CoreUserSchema(), Handler: &testResourceHandler{ data: map[string]testData{ - "0001": {attributes: map[string]interface{}{}}, + "0001": {attributes: map[string]any{}}, }, schema: schema.CoreUserSchema(), }, @@ -68,7 +68,7 @@ func newTestServer(t *testing.T) scim.Server { Schema: schema.CoreGroupSchema(), Handler: &testResourceHandler{ data: map[string]testData{ - "0001": {attributes: map[string]interface{}{}}, + "0001": {attributes: map[string]any{}}, }, schema: schema.CoreGroupSchema(), }, @@ -196,7 +196,7 @@ func (h *testResourceHandler) Patch(r *http.Request, id string, operations []sci for _, op := range operations { // Target is the root node. if op.Path == nil { - for k, v := range op.Value.(map[string]interface{}) { + for k, v := range op.Value.(map[string]any) { if v == nil { continue } @@ -204,7 +204,7 @@ func (h *testResourceHandler) Patch(r *http.Request, id string, operations []sci path, _ := filter.ParseAttrPath([]byte(k)) if subAttrName := path.SubAttributeName(); subAttrName != "" { if old, ok := h.data[id].attributes[path.AttributeName]; ok { - m := old.(map[string]interface{}) + m := old.(map[string]any) if sub, ok := m[subAttrName]; ok { if sub == v { continue @@ -216,7 +216,7 @@ func (h *testResourceHandler) Patch(r *http.Request, id string, operations []sci continue } changed = true - h.data[id].attributes[path.AttributeName] = map[string]interface{}{ + h.data[id].attributes[path.AttributeName] = map[string]any{ subAttrName: v, } continue @@ -228,11 +228,11 @@ func (h *testResourceHandler) Patch(r *http.Request, id string, operations []sci continue } switch v := v.(type) { - case []interface{}: + case []any: changed = true - h.data[id].attributes[k] = append(old.([]interface{}), v...) - case map[string]interface{}: - m := old.(map[string]interface{}) + h.data[id].attributes[k] = append(old.([]any), v...) + case map[string]any: + m := old.(map[string]any) var changed_ bool for attr, value := range v { if value == nil { @@ -274,7 +274,7 @@ func (h *testResourceHandler) Patch(r *http.Request, id string, operations []sci switch { case subAttrName != "": changed = true - h.data[id].attributes[attrName] = map[string]interface{}{ + h.data[id].attributes[attrName] = map[string]any{ subAttrName: op.Value, } case valueExpr != nil: @@ -289,13 +289,13 @@ func (h *testResourceHandler) Patch(r *http.Request, id string, operations []sci switch op.Op { case "add": switch v := op.Value.(type) { - case []interface{}: + case []any: changed = true - h.data[id].attributes[attrName] = append(old.([]interface{}), v...) + h.data[id].attributes[attrName] = append(old.([]any), v...) default: if subAttrName != "" { - m := old.(map[string]interface{}) - if value, ok := old.(map[string]interface{})[subAttrName]; ok { + m := old.(map[string]any) + if value, ok := old.(map[string]any)[subAttrName]; ok { if v == value { continue } @@ -306,8 +306,8 @@ func (h *testResourceHandler) Patch(r *http.Request, id string, operations []sci continue } switch v := v.(type) { - case map[string]interface{}: - m := old.(map[string]interface{}) + case map[string]any: + m := old.(map[string]any) var changed_ bool for attr, value := range v { if value == nil { diff --git a/service_provider_config.go b/service_provider_config.go index 45275b2..67ad8cf 100644 --- a/service_provider_config.go +++ b/service_provider_config.go @@ -61,19 +61,19 @@ func (config ServiceProviderConfig) getItemsPerPage() int { return config.MaxResults } -func (config ServiceProviderConfig) getRaw() map[string]interface{} { - return map[string]interface{}{ +func (config ServiceProviderConfig) getRaw() map[string]any { + return map[string]any{ "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"}, "documentationUri": config.DocumentationURI.Value(), "patch": map[string]bool{ "supported": config.SupportPatch, }, - "bulk": map[string]interface{}{ + "bulk": map[string]any{ "supported": false, "maxOperations": 1000, "maxPayloadSize": 1048576, }, - "filter": map[string]interface{}{ + "filter": map[string]any{ "supported": config.SupportFiltering, "maxResults": config.MaxResults, }, @@ -90,10 +90,10 @@ func (config ServiceProviderConfig) getRaw() map[string]interface{} { } } -func (config ServiceProviderConfig) getRawAuthenticationSchemes() []map[string]interface{} { - rawAuthScheme := make([]map[string]interface{}, 0) +func (config ServiceProviderConfig) getRawAuthenticationSchemes() []map[string]any { + rawAuthScheme := make([]map[string]any, 0) for _, auth := range config.AuthenticationSchemes { - rawAuthScheme = append(rawAuthScheme, map[string]interface{}{ + rawAuthScheme = append(rawAuthScheme, map[string]any{ "description": auth.Description, "documentationUri": auth.DocumentationURI.Value(), "name": auth.Name, diff --git a/utils.go b/utils.go index 326653c..10c62bf 100644 --- a/utils.go +++ b/utils.go @@ -4,13 +4,11 @@ import ( "bytes" "io" "net/http" + "slices" ) func clamp(offset, limit, length int) (int, int) { - start := length - if offset < length { - start = offset - } + start := min(offset, length) end := length if limit < length-start { end = start + limit @@ -19,13 +17,7 @@ func clamp(offset, limit, length int) (int, int) { } func contains(arr []string, el string) bool { - for _, item := range arr { - if item == el { - return true - } - } - - return false + return slices.Contains(arr, el) } func readBody(r *http.Request) ([]byte, error) { diff --git a/utils_test.go b/utils_test.go index 24ddd1d..ba6c177 100644 --- a/utils_test.go +++ b/utils_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func assertEqual(t *testing.T, expected, actual interface{}) { +func assertEqual(t *testing.T, expected, actual any) { if expected != actual { t.Errorf("not equal: expected %v, actual %v", expected, actual) } @@ -41,7 +41,7 @@ func assertFalse(t *testing.T, ok bool) { } } -func assertLen(t *testing.T, object interface{}, length int) { +func assertLen(t *testing.T, object any, length int) { ok, l := getLen(object) if !ok { t.Errorf("given object is not a slice/array") @@ -51,19 +51,19 @@ func assertLen(t *testing.T, object interface{}, length int) { } } -func assertNil(t *testing.T, object interface{}, name string) { +func assertNil(t *testing.T, object any, name string) { if object != nil { t.Errorf("object should be nil: %s", name) } } -func assertNotEqual(t *testing.T, expected, actual interface{}) { +func assertNotEqual(t *testing.T, expected, actual any) { if expected == actual { t.Errorf("%v and %v should not be equal", expected, actual) } } -func assertNotNil(t *testing.T, object interface{}, name string) { +func assertNotNil(t *testing.T, object any, name string) { if object == nil { t.Errorf("missing object: %s", name) } @@ -87,7 +87,7 @@ func assertUnmarshalNoError(t *testing.T, err error) { } } -func getLen(x interface{}) (ok bool, length int) { +func getLen(x any) (ok bool, length int) { v := reflect.ValueOf(x) defer func() { if e := recover(); e != nil {