Skip to content

Commit

Permalink
ssa: introduce jsondiff package
Browse files Browse the repository at this point in the history
Signed-off-by: Hidde Beydals <[email protected]>
  • Loading branch information
hiddeco committed Oct 3, 2023
1 parent 6dc2f17 commit 91eb1c1
Show file tree
Hide file tree
Showing 16 changed files with 2,613 additions and 4 deletions.
10 changes: 6 additions & 4 deletions ssa/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ module github.com/fluxcd/pkg/ssa

go 1.20

// Fix CVE-2022-28948
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1

require (
github.com/evanphx/json-patch/v5 v5.6.0
github.com/google/go-cmp v0.5.9
github.com/onsi/gomega v1.27.10
// TODO: unpin when https://github.com/wI2L/jsondiff/pull/14 has ended up in a release.
github.com/wI2L/jsondiff v0.4.1-0.20230626084051-c85fb8ce3cac
golang.org/x/sync v0.3.0
k8s.io/api v0.27.4
k8s.io/apimachinery v0.27.4
Expand All @@ -15,17 +21,13 @@ require (
sigs.k8s.io/yaml v1.3.0
)

// Fix CVE-2022-28948
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1

require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.2.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions ssa/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/wI2L/jsondiff v0.4.1-0.20230626084051-c85fb8ce3cac h1:X+MGDuQHQ2i4UoSsb2n4dESJoSCg7aTfvtk6Bj7nlcE=
github.com/wI2L/jsondiff v0.4.1-0.20230626084051-c85fb8ce3cac/go.mod h1:nR/vyy1efuDeAtMwc3AF6nZf/2LD1ID8GTyyJ+K8YB0=
github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk=
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
74 changes: 74 additions & 0 deletions ssa/jsondiff/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package jsondiff

import (
"github.com/wI2L/jsondiff"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// ChangeType is the type of change detected by the server-side apply diff
// operation.
type ChangeType string

const (
// ChangeTypeCreate indicates that the resource does not exist
// and needs to be created.
ChangeTypeCreate ChangeType = "create"
// ChangeTypeUpdate indicates that the resource exists and needs
// to be updated.
ChangeTypeUpdate ChangeType = "update"
// ChangeTypeExclude indicates that the resource is excluded from
// the diff.
ChangeTypeExclude ChangeType = "exclude"
// ChangeTypeNone indicates that the resource exists and is
// identical to the dry-run object.
ChangeTypeNone ChangeType = "none"
)

// Change is a change detected by the server-side apply diff operation.
type Change struct {
// Type of change detected.
Type ChangeType

// GroupVersionKind of the resource the Patch applies to.
GroupVersionKind schema.GroupVersionKind

// Namespace of the resource the Patch applies to.
Namespace string

// Name of the resource the Patch applies to.
Name string

// Patch with the changes detected for the resource.
Patch jsondiff.Patch
}

// NewChangeForUnstructured creates a new Change for the given unstructured object.
func NewChangeForUnstructured(obj *unstructured.Unstructured, t ChangeType, p jsondiff.Patch) *Change {
return &Change{
Type: t,
GroupVersionKind: obj.GetObjectKind().GroupVersionKind(),
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
Patch: p,
}
}

// ChangeSet is a list of changes.
type ChangeSet []*Change
88 changes: 88 additions & 0 deletions ssa/jsondiff/mask.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package jsondiff

import (
"github.com/wI2L/jsondiff"
"strings"
)

const (
sensitiveMaskDefault = "***"
sensitiveMaskBefore = "*** (before)"
sensitiveMaskAfter = "*** (after)"
)

// MaskSecretPatchData masks the data and stringData fields of a Secret object
// in the given JSON patch. It replaces the values with a default mask value if
// the field is added or removed. Otherwise, it replaces the values with a
// before/after mask value if the field is modified.
func MaskSecretPatchData(patch jsondiff.Patch) jsondiff.Patch {
for i := range patch {
v := &patch[i]
oldMaskValue, newMaskValue := sensitiveMaskDefault, sensitiveMaskDefault

if v.OldValue != nil && v.Value != nil {
oldMaskValue = sensitiveMaskBefore
newMaskValue = sensitiveMaskAfter
}

switch {
case v.Path == "/data" || v.Path == "/stringData":
maskMap(v.OldValue, v.Value)
case strings.HasPrefix(v.Path, "/data/") || strings.HasPrefix(v.Path, "/stringData/"):
if v.OldValue != nil {
v.OldValue = oldMaskValue
}
if v.Value != nil {
v.Value = newMaskValue
}
}
}
return patch
}

// maskMap replaces the values with a default mask value if a field is added or
// removed. Otherwise, it replaces the values with a before/after mask value if
// the field is modified.
func maskMap(from interface{}, to interface{}) {
fromMap, fromIsMap := from.(map[string]interface{})
if !fromIsMap || fromMap == nil {
fromMap = make(map[string]interface{})
}

toMap, toIsMap := to.(map[string]interface{})
if !toIsMap || toMap == nil {
toMap = make(map[string]interface{})
}

for k := range fromMap {
if _, ok := toMap[k]; ok {
if fromMap[k] != toMap[k] {
fromMap[k] = sensitiveMaskBefore
toMap[k] = sensitiveMaskAfter
continue
}
}
fromMap[k] = sensitiveMaskDefault
}
for k := range toMap {
if _, ok := fromMap[k]; !ok {
toMap[k] = sensitiveMaskDefault
}
}
}
175 changes: 175 additions & 0 deletions ssa/jsondiff/mask_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package jsondiff

import (
"reflect"
"testing"

"github.com/wI2L/jsondiff"
)

func TestMaskSecretPatchData(t *testing.T) {
tests := []struct {
name string
patch jsondiff.Patch
want jsondiff.Patch
}{
{
name: "masks replace data values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationReplace, Path: "/data/foo", OldValue: "bar", Value: "baz"},
{Type: jsondiff.OperationReplace, Path: "/data/bar", OldValue: "foo", Value: "baz"},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationReplace, Path: "/data/foo", OldValue: sensitiveMaskBefore, Value: sensitiveMaskAfter},
{Type: jsondiff.OperationReplace, Path: "/data/bar", OldValue: sensitiveMaskBefore, Value: sensitiveMaskAfter},
},
},
{
name: "masks add data values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationAdd, Path: "/data/foo", Value: "baz"},
{Type: jsondiff.OperationAdd, Path: "/data/bar", Value: "baz"},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationAdd, Path: "/data/foo", Value: sensitiveMaskDefault},
{Type: jsondiff.OperationAdd, Path: "/data/bar", Value: sensitiveMaskDefault},
},
},
{
name: "masks remove data values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationRemove, Path: "/data/foo", OldValue: "bar"},
{Type: jsondiff.OperationRemove, Path: "/data/bar", OldValue: "foo"},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationRemove, Path: "/data/foo", OldValue: sensitiveMaskDefault},
{Type: jsondiff.OperationRemove, Path: "/data/bar", OldValue: sensitiveMaskDefault},
},
},
{
name: "masks rationalized replace data values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationReplace, Path: "/data", OldValue: map[string]interface{}{
"foo": "bar",
"bar": "foo",
}, Value: map[string]interface{}{
"foo": "baz",
"bar": "baz",
}},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationReplace, Path: "/data", OldValue: map[string]interface{}{
"foo": sensitiveMaskBefore,
"bar": sensitiveMaskBefore,
}, Value: map[string]interface{}{
"foo": sensitiveMaskAfter,
"bar": sensitiveMaskAfter,
},
}},
},
{
name: "masks rationalized add data values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationAdd, Path: "/data", Value: map[string]interface{}{
"foo": "baz",
"bar": "baz",
}},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationAdd, Path: "/data", Value: map[string]interface{}{
"foo": sensitiveMaskDefault,
"bar": sensitiveMaskDefault,
}},
},
},
{
name: "masks rationalized remove data values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationRemove, Path: "/data", OldValue: map[string]interface{}{
"foo": "bar",
"bar": "foo",
}},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationRemove, Path: "/data", OldValue: map[string]interface{}{
"foo": sensitiveMaskDefault,
"bar": sensitiveMaskDefault,
}},
},
},
{
name: "masks rationalized replace complex data values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationReplace, Path: "/data", OldValue: map[string]interface{}{
// Changed key
"foo": "bar",
// Removed key
"bar": "baz",
}, Value: map[string]interface{}{
"foo": "baz",
// Added key
"baz": "bar",
}},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationReplace, Path: "/data", OldValue: map[string]interface{}{
"foo": sensitiveMaskBefore,
"bar": sensitiveMaskDefault,
}, Value: map[string]interface{}{
"foo": sensitiveMaskAfter,
"baz": sensitiveMaskDefault,
}},
},
},
{
name: "masks replace stringData values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationReplace, Path: "/stringData/foo", OldValue: "bar", Value: "baz"},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationReplace, Path: "/stringData/foo", OldValue: sensitiveMaskBefore, Value: sensitiveMaskAfter},
},
},
{
name: "masks add stringData values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationAdd, Path: "/stringData/foo", Value: "baz"},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationAdd, Path: "/stringData/foo", Value: sensitiveMaskDefault},
},
},
{
name: "masks remove stringData values",
patch: jsondiff.Patch{
{Type: jsondiff.OperationRemove, Path: "/stringData/foo", OldValue: "bar"},
},
want: jsondiff.Patch{
{Type: jsondiff.OperationRemove, Path: "/stringData/foo", OldValue: sensitiveMaskDefault},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := MaskSecretPatchData(tt.patch); !reflect.DeepEqual(got, tt.want) {
t.Errorf("maskUnstructuredSecretData() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit 91eb1c1

Please sign in to comment.