Skip to content

Commit fa485d6

Browse files
committed
fix: return noTarget error for replace with unmatched value expression
1 parent 162f25b commit fa485d6

3 files changed

Lines changed: 74 additions & 8 deletions

File tree

patch_apply.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ package scim
33
import (
44
"fmt"
55

6+
scimErrors "github.com/elimity-com/scim/errors"
67
internal "github.com/elimity-com/scim/filter"
78
"github.com/elimity-com/scim/schema"
89
filter "github.com/scim2/filter-parser/v2"
910
)
1011

11-
func applyToMatching(list []interface{}, t *valueExprTarget, value interface{}, replace bool) ([]interface{}, error) {
12+
func applyToMatching(list []interface{}, t *valueExprTarget, value interface{}, replace bool) ([]interface{}, bool, error) {
1213
result := make([]interface{}, len(list))
1314
copy(result, list)
1415

16+
matched := false
1517
for i, elem := range result {
1618
m, ok := elem.(map[string]interface{})
1719
if !ok {
@@ -20,6 +22,7 @@ func applyToMatching(list []interface{}, t *valueExprTarget, value interface{},
2022
if !matchesExpression(m, t.expr, t.attr, t.refSchema) {
2123
continue
2224
}
25+
matched = true
2326
if t.subAttrName != "" {
2427
updated := copyMap(m)
2528
updated[t.subAttrName] = value
@@ -49,7 +52,7 @@ func applyToMatching(list []interface{}, t *valueExprTarget, value interface{},
4952
}
5053
}
5154
}
52-
return result, nil
55+
return result, matched, nil
5356
}
5457

5558
func copyMap(m map[string]interface{}) map[string]interface{} {
@@ -263,7 +266,7 @@ func applyAdd(attrs ResourceAttributes, op PatchOperation, s schema.Schema, exte
263266
if !ok {
264267
return nil, fmt.Errorf("expected multi-valued attribute for %s", attrName)
265268
}
266-
list, err := applyToMatching(list, t, op.Value, false)
269+
list, _, err := applyToMatching(list, t, op.Value, false)
267270
if err != nil {
268271
return nil, err
269272
}
@@ -405,18 +408,25 @@ func applyReplace(attrs ResourceAttributes, op PatchOperation, s schema.Schema,
405408
m[t.subAttrName] = op.Value
406409
attrs[attrName] = m
407410
case *valueExprTarget:
411+
// RFC 7644 Section 3.5.2.3: if the target location is a multi-valued
412+
// attribute for which a value selection filter ("valuePath") has been
413+
// supplied and no record match was made, the service provider SHALL
414+
// return a 400 error with a scimType of noTarget.
408415
existing, exists := attrs[attrName]
409416
if !exists {
410-
return attrs, nil
417+
return nil, scimErrors.ScimErrorNoTarget
411418
}
412419
list, ok := existing.([]interface{})
413420
if !ok {
414421
return nil, fmt.Errorf("expected multi-valued attribute for %s", attrName)
415422
}
416-
list, err := applyToMatching(list, t, op.Value, true)
423+
list, matched, err := applyToMatching(list, t, op.Value, true)
417424
if err != nil {
418425
return nil, err
419426
}
427+
if !matched {
428+
return nil, scimErrors.ScimErrorNoTarget
429+
}
420430
attrs[attrName] = list
421431
}
422432
return attrs, nil

patch_apply_test.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package scim
33
import (
44
"testing"
55

6+
scimErrors "github.com/elimity-com/scim/errors"
67
"github.com/elimity-com/scim/schema"
7-
filterlib "github.com/scim2/filter-parser/v2"
8+
filter "github.com/scim2/filter-parser/v2"
89
)
910

1011
func TestApplyPatch_AddSimpleAttribute(t *testing.T) {
@@ -306,6 +307,47 @@ func TestApplyPatch_ReplaceSimpleAttribute(t *testing.T) {
306307
}
307308
}
308309

310+
// RFC 7644 Section 3.5.2.3: "If the target location is a multi-valued
311+
// attribute for which a value selection filter ("valuePath") has been
312+
// supplied and no record match was made, the service provider SHALL
313+
// return a 400 error with a scimType of noTarget".
314+
func TestApplyPatch_ReplaceValueExprNoTarget_AttributeMissing(t *testing.T) {
315+
s := testUserSchema()
316+
attrs := ResourceAttributes{
317+
"userName": "john",
318+
}
319+
320+
_, err := ApplyPatch(attrs, []PatchOperation{
321+
{
322+
Op: PatchOperationReplace,
323+
Path: mustParsePath(`emails[type eq "work"].value`),
324+
Value: "new@work.com",
325+
},
326+
}, s)
327+
assertScimError(t, err, scimErrors.ScimTypeNoTarget)
328+
}
329+
330+
func TestApplyPatch_ReplaceValueExprNoTarget_NoMatch(t *testing.T) {
331+
s := testUserSchema()
332+
attrs := ResourceAttributes{
333+
"emails": []interface{}{
334+
map[string]interface{}{
335+
"value": "john@home.com",
336+
"type": "home",
337+
},
338+
},
339+
}
340+
341+
_, err := ApplyPatch(attrs, []PatchOperation{
342+
{
343+
Op: PatchOperationReplace,
344+
Path: mustParsePath(`emails[type eq "work"].value`),
345+
Value: "new@work.com",
346+
},
347+
}, s)
348+
assertScimError(t, err, scimErrors.ScimTypeNoTarget)
349+
}
350+
309351
func TestApplyPatch_ReplaceWithNoPath(t *testing.T) {
310352
s := testUserSchema()
311353
attrs := ResourceAttributes{
@@ -374,8 +416,8 @@ func TestApplyPatch_ReplaceWithValueExpression(t *testing.T) {
374416
}
375417
}
376418

377-
func mustParsePath(s string) *filterlib.Path {
378-
p, err := filterlib.ParsePath([]byte(s))
419+
func mustParsePath(s string) *filter.Path {
420+
p, err := filter.ParsePath([]byte(s))
379421
if err != nil {
380422
panic(err)
381423
}

utils_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ func assertNotNil(t *testing.T, object interface{}, name string) {
6969
}
7070
}
7171

72+
func assertScimError(t *testing.T, err error, expectedType errors.ScimType) {
73+
t.Helper()
74+
if err == nil {
75+
t.Fatalf("expected scimType %q error, got nil", expectedType)
76+
}
77+
scimErr, ok := err.(errors.ScimError)
78+
if !ok {
79+
t.Fatalf("expected ScimError, got %T: %v", err, err)
80+
}
81+
if scimErr.ScimType != expectedType {
82+
t.Errorf("expected scimType %q, got %q", expectedType, scimErr.ScimType)
83+
}
84+
}
85+
7286
func assertTrue(t *testing.T, ok bool) {
7387
if !ok {
7488
t.Error("value should be true")

0 commit comments

Comments
 (0)