mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:37:31 +00:00
feat: patch user scim v2 endpoint (#9219)
# Which Problems Are Solved * Adds support for the patch user SCIM v2 endpoint # How the Problems Are Solved * Adds support for the patch user SCIM v2 endpoint under `PATCH /scim/v2/{orgID}/Users/{id}` # Additional Context Part of #8140
This commit is contained in:
273
internal/api/scim/resources/patch/patch.go
Normal file
273
internal/api/scim/resources/patch/patch.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/scim/resources/filter"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/schemas"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/serrors"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type OperationRequest struct {
|
||||
Schemas []schemas.ScimSchemaType `json:"Schemas"`
|
||||
Operations []*Operation `json:"Operations"`
|
||||
}
|
||||
|
||||
type Operation struct {
|
||||
Operation OperationType `json:"op"`
|
||||
Path *filter.Path `json:"path"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
valueIsArray bool
|
||||
}
|
||||
|
||||
type OperationCollection []*Operation
|
||||
|
||||
type OperationType string
|
||||
|
||||
const (
|
||||
OperationTypeAdd OperationType = "add"
|
||||
OperationTypeRemove OperationType = "remove"
|
||||
OperationTypeReplace OperationType = "replace"
|
||||
|
||||
fieldNamePrimary = "Primary"
|
||||
fieldNameValue = "Value"
|
||||
)
|
||||
|
||||
type ResourcePatcher interface {
|
||||
FilterEvaluator() *filter.Evaluator
|
||||
Added(attributePath []string) error
|
||||
Replaced(attributePath []string) error
|
||||
Removed(attributePath []string) error
|
||||
}
|
||||
|
||||
func (req *OperationRequest) Validate() error {
|
||||
if !slices.Contains(req.Schemas, schemas.IdPatchOperation) {
|
||||
return serrors.ThrowInvalidSyntax(zerrors.ThrowInvalidArgumentf(nil, "SCIM-xy1schema", "Expected schema %v is not provided", schemas.IdPatchOperation))
|
||||
}
|
||||
|
||||
for _, op := range req.Operations {
|
||||
if err := op.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (op *Operation) validate() error {
|
||||
if !op.Operation.isValid() {
|
||||
return serrors.ThrowInvalidValue(zerrors.ThrowInvalidArgumentf(nil, "SCIM-opty1", "Patch op %s not supported", op.Operation))
|
||||
}
|
||||
|
||||
// json deserialization initializes this field if an empty string is provided
|
||||
// to not special case this in the further code,
|
||||
// set it to nil here.
|
||||
if op.Path.IsZero() {
|
||||
op.Path = nil
|
||||
}
|
||||
|
||||
op.valueIsArray = strings.HasPrefix(strings.TrimPrefix(string(op.Value), " "), "[")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ops OperationCollection) Apply(patcher ResourcePatcher, value interface{}) error {
|
||||
for _, op := range ops {
|
||||
if err := op.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := op.apply(patcher, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (op *Operation) apply(patcher ResourcePatcher, value interface{}) error {
|
||||
switch op.Operation {
|
||||
case OperationTypeRemove:
|
||||
return applyRemovePatch(patcher, op, value)
|
||||
case OperationTypeReplace:
|
||||
return applyReplacePatch(patcher, op, value)
|
||||
case OperationTypeAdd:
|
||||
return applyAddPatch(patcher, op, value)
|
||||
}
|
||||
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "SCIM-opty3", "SCIM patch: Invalid operation %v", op.Operation)
|
||||
}
|
||||
|
||||
func (o OperationType) isValid() bool {
|
||||
switch o {
|
||||
case OperationTypeAdd, OperationTypeRemove, OperationTypeReplace:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func flattenAndApplyPatchOperations(patcher ResourcePatcher, op *Operation, value interface{}) error {
|
||||
ops, err := flattenPatchOperations(op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, flattenedOperation := range ops {
|
||||
if err = flattenedOperation.apply(patcher, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// flattenPatchOperations flattens patch operations without a path
|
||||
// it converts an op { "operation": "add", "value": { "path1": "value1", "path2": "value2" } }
|
||||
// into [ { "operation": "add", "path": "path1", "value": "value1" }, { "operation": "add", "path": "path2", "value": "value2" } ]
|
||||
func flattenPatchOperations(op *Operation) ([]*Operation, error) {
|
||||
if op.Path != nil {
|
||||
panic("Only operations without a path can be flattened")
|
||||
}
|
||||
|
||||
patches := map[string]json.RawMessage{}
|
||||
if err := json.Unmarshal(op.Value, &patches); err != nil {
|
||||
logging.WithError(err).Error("SCIM: Invalid patch value while flattening")
|
||||
return nil, zerrors.ThrowInvalidArgument(err, "SCIM-ioyl1", "Invalid patch value")
|
||||
}
|
||||
|
||||
result := make([]*Operation, 0, len(patches))
|
||||
for path, value := range patches {
|
||||
result = append(result, &Operation{
|
||||
Operation: op.Operation,
|
||||
Path: &filter.Path{
|
||||
AttrPath: &filter.AttrPath{
|
||||
AttrName: path,
|
||||
},
|
||||
},
|
||||
Value: value,
|
||||
valueIsArray: strings.HasPrefix(string(value), "["),
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// unmarshalPatchValuesSlice unmarshal the raw json value (a scalar value, object or array) into a new slice
|
||||
func unmarshalPatchValuesSlice(elementTypePtr reflect.Type, value json.RawMessage, valueIsArray bool) (reflect.Value, error) {
|
||||
if elementTypePtr.Kind() != reflect.Ptr {
|
||||
logging.Panicf("elementType must be a pointer to a struct, but is %s", elementTypePtr.Name())
|
||||
return reflect.Value{}, nil
|
||||
}
|
||||
|
||||
if !valueIsArray {
|
||||
newElement := reflect.New(elementTypePtr.Elem())
|
||||
if err := unmarshalPatchValue(value, newElement); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
|
||||
newSlice := reflect.MakeSlice(reflect.SliceOf(elementTypePtr), 1, 1)
|
||||
newSlice.Index(0).Set(newElement)
|
||||
return newSlice, nil
|
||||
}
|
||||
|
||||
newSlicePtr := reflect.New(reflect.SliceOf(elementTypePtr))
|
||||
newSlice := newSlicePtr.Elem()
|
||||
if err := json.Unmarshal(value, newSlicePtr.Interface()); err != nil {
|
||||
logging.WithError(err).Error("SCIM: Invalid patch values")
|
||||
return reflect.Value{}, zerrors.ThrowInvalidArgument(err, "SCIM-opxx8", "Invalid patch values")
|
||||
}
|
||||
return newSlice, nil
|
||||
}
|
||||
|
||||
func unmarshalPatchValue(newValue json.RawMessage, targetElement reflect.Value) error {
|
||||
if targetElement.Kind() != reflect.Ptr {
|
||||
targetElement = targetElement.Addr()
|
||||
}
|
||||
|
||||
if targetElement.IsNil() {
|
||||
targetElement.Set(reflect.New(targetElement.Type().Elem()))
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(newValue, targetElement.Interface()); err != nil {
|
||||
logging.WithError(err).Error("SCIM: Invalid patch value")
|
||||
return zerrors.ThrowInvalidArgument(err, "SCIM-opty9", "Invalid patch value")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureSinglePrimary ensures the modification on a slice results in max one primary element.
|
||||
// modifiedSlice contains the patched slice.
|
||||
// modifiedElementsSlice contains only the modified elements.
|
||||
// if a new element has Primary set to true and an existing is also Primary, the existing Primary flag is set to false.
|
||||
// returns an error if multiple modifiedElements have a primary value of true.
|
||||
func ensureSinglePrimary(modifiedSlice reflect.Value, modifiedElementsSlice []reflect.Value, modifiedElementIndexes map[int]bool) error {
|
||||
if len(modifiedElementsSlice) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hasPrimary, err := isAnyPrimary(modifiedElementsSlice)
|
||||
if err != nil || !hasPrimary {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < modifiedSlice.Len(); i++ {
|
||||
if mod, ok := modifiedElementIndexes[i]; ok && mod {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceElement := modifiedSlice.Index(i)
|
||||
if sliceElement.Kind() == reflect.Ptr {
|
||||
sliceElement = sliceElement.Elem()
|
||||
}
|
||||
|
||||
sliceElementPrimaryField := sliceElement.FieldByName(fieldNamePrimary)
|
||||
if !sliceElementPrimaryField.IsValid() || !sliceElementPrimaryField.Bool() {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceElementPrimaryField.SetBool(false)
|
||||
|
||||
// we can stop at the first primary,
|
||||
// since there can only be one primary in a slice.
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isAnyPrimary(elements []reflect.Value) (bool, error) {
|
||||
foundPrimary := false
|
||||
for _, element := range elements {
|
||||
if !isPrimary(element) {
|
||||
continue
|
||||
}
|
||||
|
||||
if foundPrimary {
|
||||
return true, zerrors.ThrowInvalidArgument(nil, "SCIM-1d23", "Cannot add multiple primary values in one patch operation")
|
||||
}
|
||||
|
||||
foundPrimary = true
|
||||
}
|
||||
|
||||
return foundPrimary, nil
|
||||
}
|
||||
|
||||
func isPrimary(element reflect.Value) bool {
|
||||
if element.Kind() == reflect.Ptr {
|
||||
element = element.Elem()
|
||||
}
|
||||
|
||||
if element.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
|
||||
primaryField := element.FieldByName(fieldNamePrimary)
|
||||
return primaryField.IsValid() && primaryField.Bool()
|
||||
}
|
112
internal/api/scim/resources/patch/patch_add.go
Normal file
112
internal/api/scim/resources/patch/patch_add.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/scim/resources/filter"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/serrors"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func applyAddPatch(patcher ResourcePatcher, op *Operation, value interface{}) error {
|
||||
if op.Path == nil {
|
||||
return flattenAndApplyPatchOperations(patcher, op, value)
|
||||
}
|
||||
|
||||
result, err := patcher.FilterEvaluator().Evaluate(reflect.ValueOf(value), op.Path)
|
||||
if err != nil {
|
||||
return serrors.ThrowInvalidPath(zerrors.ThrowInvalidArgument(err, "SCIM-opzz8", "Failed to evaluate path"))
|
||||
}
|
||||
|
||||
evaluationResult, ok := result.(*filter.SimpleValueEvaluationResult)
|
||||
if !ok {
|
||||
return serrors.ThrowInvalidPath(zerrors.ThrowInvalidArgument(nil, "SCIM-opty8", "Filtered paths are not allowed for add patch operations"))
|
||||
}
|
||||
|
||||
if evaluationResult.Value.Kind() != reflect.Slice {
|
||||
return applyReplacePatchSimple(patcher, evaluationResult, op.Value, op.valueIsArray)
|
||||
}
|
||||
|
||||
elementType := evaluationResult.Value.Type().Elem()
|
||||
newElementsSlice, err := unmarshalPatchValuesSlice(elementType, op.Value, op.valueIsArray)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldLen := evaluationResult.Value.Len()
|
||||
newSlice := reflect.MakeSlice(reflect.SliceOf(elementType), oldLen, oldLen+newElementsSlice.Len())
|
||||
|
||||
// copy over existing values
|
||||
reflect.Copy(newSlice, evaluationResult.Value)
|
||||
|
||||
// according to the RFC only "new" values should be added
|
||||
// existing values should be replaced
|
||||
newSlice, modifiedIndexes := addOrReplaceByValue(newSlice, newElementsSlice)
|
||||
|
||||
evaluationResult.Value.Set(newSlice)
|
||||
if err = ensureSinglePrimaryAdded(evaluationResult.Value, newElementsSlice, modifiedIndexes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return patcher.Added(evaluationResult.PathSegments)
|
||||
}
|
||||
|
||||
func ensureSinglePrimaryAdded(resultSlice, newSlice reflect.Value, modifiedIndexes map[int]bool) error {
|
||||
modifiedValues := make([]reflect.Value, newSlice.Len())
|
||||
for i := 0; i < newSlice.Len(); i++ {
|
||||
modifiedValues[i] = newSlice.Index(i)
|
||||
}
|
||||
|
||||
return ensureSinglePrimary(resultSlice, modifiedValues, modifiedIndexes)
|
||||
}
|
||||
|
||||
func addOrReplaceByValue(entries, newEntries reflect.Value) (reflect.Value, map[int]bool) {
|
||||
modifiedIndexes := make(map[int]bool, newEntries.Len())
|
||||
if entries.Len() == 0 {
|
||||
for i := 0; i < newEntries.Len(); i++ {
|
||||
modifiedIndexes[i] = true
|
||||
}
|
||||
|
||||
return newEntries, modifiedIndexes
|
||||
}
|
||||
|
||||
valueField := entries.Index(0).Elem().FieldByName(fieldNameValue)
|
||||
if !valueField.IsValid() || valueField.Kind() != reflect.String {
|
||||
entriesLen := entries.Len()
|
||||
for i := 0; i < newEntries.Len(); i++ {
|
||||
modifiedIndexes[i+entriesLen] = true
|
||||
}
|
||||
|
||||
return reflect.AppendSlice(entries, newEntries), modifiedIndexes
|
||||
}
|
||||
|
||||
existingValueIndexes := make(map[string]int, entries.Len())
|
||||
for i := 0; i < entries.Len(); i++ {
|
||||
value := entries.Index(i).Elem().FieldByName(fieldNameValue).String()
|
||||
if _, ok := existingValueIndexes[value]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
existingValueIndexes[value] = i
|
||||
}
|
||||
|
||||
entriesLen := entries.Len()
|
||||
for i := 0; i < newEntries.Len(); i++ {
|
||||
newEntry := newEntries.Index(i)
|
||||
value := newEntry.Elem().FieldByName(fieldNameValue).String()
|
||||
index, valueExists := existingValueIndexes[value]
|
||||
|
||||
// according to the rfc if the value already exists it should be replaced
|
||||
if valueExists {
|
||||
entries.Index(index).Set(newEntry)
|
||||
modifiedIndexes[index] = true
|
||||
continue
|
||||
}
|
||||
|
||||
entries = reflect.Append(entries, newEntry)
|
||||
modifiedIndexes[entriesLen] = true
|
||||
entriesLen++
|
||||
}
|
||||
|
||||
return entries, modifiedIndexes
|
||||
}
|
73
internal/api/scim/resources/patch/patch_remove.go
Normal file
73
internal/api/scim/resources/patch/patch_remove.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/scim/resources/filter"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/serrors"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func applyRemovePatch(patcher ResourcePatcher, op *Operation, value interface{}) error {
|
||||
// the root cannot be removed
|
||||
if op.Path == nil {
|
||||
logging.Info("SCIM: remove patch without path")
|
||||
return serrors.ThrowNoTarget(zerrors.ThrowInvalidArgument(nil, "SCIM-ozzy54", "Remove patch without path is not supported"))
|
||||
}
|
||||
|
||||
result, err := patcher.FilterEvaluator().Evaluate(reflect.ValueOf(value), op.Path)
|
||||
if err != nil {
|
||||
return serrors.ThrowInvalidPath(zerrors.ThrowInvalidArgument(err, "SCIM-sd41", "Failed to evaluate path"))
|
||||
}
|
||||
|
||||
switch filterResult := result.(type) {
|
||||
case *filter.SimpleValueEvaluationResult:
|
||||
return applyRemovePatchSimple(patcher, filterResult)
|
||||
case *filter.FilteredValuesEvaluationResult:
|
||||
return applyRemovePatchFiltered(patcher, filterResult)
|
||||
}
|
||||
|
||||
logging.Errorf("SCIM remove patch: unsupported filter type %T", result)
|
||||
return serrors.ThrowInvalidPath(zerrors.ThrowInvalidArgument(err, "SCIM-12syw", "Invalid patch path"))
|
||||
}
|
||||
|
||||
func applyRemovePatchSimple(patcher ResourcePatcher, filterResult *filter.SimpleValueEvaluationResult) error {
|
||||
filterResult.Value.Set(reflect.Zero(filterResult.Value.Type()))
|
||||
return patcher.Removed(filterResult.PathSegments)
|
||||
}
|
||||
|
||||
func applyRemovePatchFiltered(patcher ResourcePatcher, filterResult *filter.FilteredValuesEvaluationResult) error {
|
||||
if len(filterResult.Matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if a subattribute is selected, set that one to nil instead of removing the elements from the slice
|
||||
if len(filterResult.PathSegments) > 1 {
|
||||
for _, match := range filterResult.Matches {
|
||||
match.Value.Set(reflect.Zero(match.Value.Type()))
|
||||
}
|
||||
|
||||
return patcher.Removed(filterResult.PathSegments)
|
||||
}
|
||||
|
||||
slice := filterResult.Source
|
||||
sliceLen := slice.Len()
|
||||
|
||||
// if all elements are matched, set the field to nil
|
||||
if sliceLen == len(filterResult.Matches) {
|
||||
filterResult.Source.Set(reflect.Zero(slice.Type()))
|
||||
return patcher.Removed(filterResult.PathSegments)
|
||||
}
|
||||
|
||||
// start at the very last matched value to keep correct indexing
|
||||
for i := len(filterResult.Matches) - 1; i >= 0; i-- {
|
||||
match := filterResult.Matches[i]
|
||||
slice = reflect.AppendSlice(slice.Slice(0, match.SourceIndex), slice.Slice(match.SourceIndex+1, sliceLen))
|
||||
sliceLen--
|
||||
}
|
||||
|
||||
filterResult.Source.Set(slice)
|
||||
return patcher.Removed(filterResult.PathSegments)
|
||||
}
|
105
internal/api/scim/resources/patch/patch_replace.go
Normal file
105
internal/api/scim/resources/patch/patch_replace.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/scim/resources/filter"
|
||||
"github.com/zitadel/zitadel/internal/api/scim/serrors"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func applyReplacePatch(patcher ResourcePatcher, op *Operation, value interface{}) error {
|
||||
if op.Path == nil {
|
||||
return flattenAndApplyPatchOperations(patcher, op, value)
|
||||
}
|
||||
|
||||
result, err := patcher.FilterEvaluator().Evaluate(reflect.ValueOf(value), op.Path)
|
||||
if err != nil {
|
||||
return serrors.ThrowInvalidPath(zerrors.ThrowInvalidArgument(err, "SCIM-i2o3", "Failed to evaluate path"))
|
||||
}
|
||||
|
||||
switch filterResult := result.(type) {
|
||||
case *filter.SimpleValueEvaluationResult:
|
||||
return applyReplacePatchSimple(patcher, filterResult, op.Value, op.valueIsArray)
|
||||
case *filter.FilteredValuesEvaluationResult:
|
||||
return applyReplacePatchFiltered(patcher, filterResult, op.Value)
|
||||
}
|
||||
|
||||
logging.Errorf("SCIM replace patch: unsupported filter type %T", result)
|
||||
return serrors.ThrowInvalidPath(zerrors.ThrowInvalidArgument(err, "SCIM-optu9", "Invalid patch path"))
|
||||
}
|
||||
|
||||
func applyReplacePatchSimple(patcher ResourcePatcher, evaluationResult *filter.SimpleValueEvaluationResult, newValueRaw json.RawMessage, valueIsArray bool) error {
|
||||
// patch value is an array
|
||||
// or it is a scalar or an object but the target is a slice
|
||||
// unmarshal it as a slice and set it on the target, clearing all existing entries
|
||||
if valueIsArray || evaluationResult.Value.Kind() == reflect.Slice {
|
||||
return applyReplacePatchSimpleSlice(patcher, evaluationResult, newValueRaw, valueIsArray)
|
||||
}
|
||||
|
||||
// patch value and target is a scalar or object
|
||||
if err := unmarshalPatchValue(newValueRaw, evaluationResult.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return patcher.Replaced(evaluationResult.PathSegments)
|
||||
}
|
||||
|
||||
func applyReplacePatchSimpleSlice(patcher ResourcePatcher, evaluationResult *filter.SimpleValueEvaluationResult, newValueRaw json.RawMessage, valueIsArray bool) error {
|
||||
if evaluationResult.Value.Kind() != reflect.Slice {
|
||||
return zerrors.ThrowInvalidArgument(nil, "SCIM-E345X", "Cannot apply array patch value to single value attribute")
|
||||
}
|
||||
|
||||
values, err := unmarshalPatchValuesSlice(evaluationResult.Value.Type().Elem(), newValueRaw, valueIsArray)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evaluationResult.Value.Set(values)
|
||||
modifiedIndexes := make(map[int]bool, values.Len())
|
||||
for i := 0; i < values.Len(); i++ {
|
||||
modifiedIndexes[i] = true
|
||||
}
|
||||
|
||||
if err = ensureSinglePrimaryAdded(values, values, modifiedIndexes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return patcher.Replaced(evaluationResult.PathSegments)
|
||||
}
|
||||
|
||||
func applyReplacePatchFiltered(patcher ResourcePatcher, result *filter.FilteredValuesEvaluationResult, newValueRaw json.RawMessage) error {
|
||||
if len(result.Matches) == 0 {
|
||||
return serrors.ThrowNoTarget(zerrors.ThrowInvalidArgument(nil, "SCIM-4513", "Path evaluation resulted in no matches"))
|
||||
}
|
||||
|
||||
for _, match := range result.Matches {
|
||||
if err := unmarshalPatchValue(newValueRaw, match.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := ensureSinglePrimaryBasedOnMatches(result.Source, result.Matches); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return patcher.Replaced(result.PathSegments)
|
||||
}
|
||||
|
||||
func ensureSinglePrimaryBasedOnMatches(slice reflect.Value, matches []*filter.FilteredValuesEvaluationResultMatch) error {
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
modifiedElements := make([]reflect.Value, 0, len(matches))
|
||||
modifiedIndexes := make(map[int]bool, len(matches))
|
||||
for _, match := range matches {
|
||||
modifiedElements = append(modifiedElements, match.Element)
|
||||
modifiedIndexes[match.SourceIndex] = true
|
||||
}
|
||||
|
||||
return ensureSinglePrimary(slice, modifiedElements, modifiedIndexes)
|
||||
}
|
Reference in New Issue
Block a user