-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[upstream commit 568dbc5] This commit is to add a generic json field masking based on the field name. [ Quentin: Replaced the use of slices.Contains(), unsupported with the Go version used with v1.13. ] Signed-off-by: Tam Mach <tam.mach@cilium.io> Signed-off-by: Quentin Monnet <qmo@qmon.net>
- Loading branch information
1 parent
dc19b3d
commit 958d7b7
Showing
3 changed files
with
258 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright Authors of Cilium | ||
|
||
package cmd | ||
|
||
import ( | ||
"encoding/json" | ||
) | ||
|
||
const ( | ||
redacted = "[redacted]" | ||
ident = "\t" | ||
) | ||
|
||
// jsonFieldMaskPostProcess returns a postProcessFunc that masks the specified field names. | ||
// The input byte slice is expected to be a JSON object. | ||
func jsonFieldMaskPostProcess(fieldNames []string) postProcessFunc { | ||
return func(b []byte) ([]byte, error) { | ||
return maskFields(b, fieldNames) | ||
} | ||
} | ||
|
||
func maskFields(b []byte, fieldNames []string) ([]byte, error) { | ||
var data map[string]interface{} | ||
|
||
if err := json.Unmarshal(b, &data); err != nil { | ||
return nil, err | ||
} | ||
|
||
mask(data, fieldNames) | ||
|
||
// MarshalIndent is used to make the output more readable. | ||
return json.MarshalIndent(data, "", ident) | ||
} | ||
|
||
func contains(names []string, name string) bool { | ||
for _, n := range names { | ||
if n == name { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func mask(data map[string]interface{}, fieldNames []string) { | ||
for k, v := range data { | ||
if contains(fieldNames, k) { | ||
data[k] = redacted | ||
continue | ||
} | ||
|
||
switch t := v.(type) { | ||
case map[string]interface{}: | ||
mask(t, fieldNames) | ||
case []interface{}: | ||
for i, item := range t { | ||
if subData, ok := item.(map[string]interface{}); ok { | ||
mask(subData, fieldNames) | ||
t[i] = subData | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright Authors of Cilium | ||
|
||
package cmd | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func Test_jsonFieldMaskPostProcess(t *testing.T) { | ||
type args struct { | ||
input []byte | ||
fieldNames []string | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want []byte | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "simple struct", | ||
args: args{ | ||
input: []byte(`{ | ||
"username": "user1", | ||
"password": "mypassword", | ||
"email": "user1@example.com" | ||
}`), | ||
fieldNames: []string{"password"}, | ||
}, | ||
want: []byte(`{ | ||
"username": "user1", | ||
"password": "[redacted]", | ||
"email": "user1@example.com" | ||
}`), | ||
}, | ||
{ | ||
name: "array struct", | ||
args: args{ | ||
input: []byte(`{ | ||
"username": "user1", | ||
"secrets": [ | ||
{ | ||
"password": "mypassword" | ||
}, | ||
{ | ||
"password": "anotherone" | ||
} | ||
], | ||
"password": "mypassword", | ||
"email": "user1@example.com" | ||
}`), | ||
fieldNames: []string{"password"}, | ||
}, | ||
want: []byte(`{ | ||
"username": "user1", | ||
"secrets": [ | ||
{ | ||
"password": "[redacted]" | ||
}, | ||
{ | ||
"password": "[redacted]" | ||
} | ||
], | ||
"password": "[redacted]", | ||
"email": "user1@example.com" | ||
}`), | ||
}, | ||
{ | ||
name: "nested struct", | ||
args: args{ | ||
input: []byte(`{ | ||
"username": "user1", | ||
"password": "mypassword", | ||
"email": "user1@example.com", | ||
"complex": { | ||
"password": "anotherpassword" | ||
} | ||
}`), | ||
fieldNames: []string{"password"}, | ||
}, | ||
want: []byte(`{ | ||
"username": "user1", | ||
"password": "[redacted]", | ||
"email": "user1@example.com", | ||
"complex": { | ||
"password": "[redacted]" | ||
} | ||
}`), | ||
}, | ||
{ | ||
name: "nested array struct", | ||
args: args{ | ||
input: []byte(`{ | ||
"username": "user1", | ||
"password": "mypassword", | ||
"email": "user1@example.com", | ||
"complex": { | ||
"secrets": [ | ||
{ | ||
"password": "mypassword" | ||
}, | ||
{ | ||
"password": "anotherpassword" | ||
} | ||
] | ||
} | ||
}`), | ||
fieldNames: []string{"password"}, | ||
}, | ||
want: []byte(`{ | ||
"username": "user1", | ||
"password": "[redacted]", | ||
"email": "user1@example.com", | ||
"complex": { | ||
"secrets": [ | ||
{ | ||
"password": "[redacted]" | ||
}, | ||
{ | ||
"password": "[redacted]" | ||
} | ||
] | ||
} | ||
}`), | ||
}, | ||
{ | ||
name: "no masked field", | ||
args: args{ | ||
input: []byte(`{ | ||
"username": "user1", | ||
"password": "mypassword", | ||
"email": "user1@example.com", | ||
"complex": { | ||
"password": "anotherpassword" | ||
} | ||
}`), | ||
fieldNames: []string{"no-such-field"}, | ||
}, | ||
want: []byte(`{ | ||
"username": "user1", | ||
"password": "mypassword", | ||
"email": "user1@example.com", | ||
"complex": { | ||
"password": "anotherpassword" | ||
} | ||
}`), | ||
}, | ||
{ | ||
name: "mask object field", | ||
args: args{ | ||
input: []byte(`{ | ||
"username": "user1", | ||
"password": "mypassword", | ||
"email": "user1@example.com", | ||
"complex": { | ||
"password": "anotherpassword" | ||
} | ||
}`), | ||
fieldNames: []string{"complex"}, | ||
}, | ||
want: []byte(`{ | ||
"username": "user1", | ||
"password": "mypassword", | ||
"email": "user1@example.com", | ||
"complex": "[redacted]" | ||
}`), | ||
}, | ||
{ | ||
name: "invalid input", | ||
args: args{ | ||
input: []byte(`{"username": "user1",}`), | ||
fieldNames: []string{"password"}, | ||
}, | ||
want: nil, | ||
wantErr: true, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := jsonFieldMaskPostProcess(tt.args.fieldNames)(tt.args.input) | ||
require.Equal(t, tt.wantErr, err != nil) | ||
// only assert the output if there is no error | ||
// as JSONEq func is used to compare the output | ||
if !tt.wantErr { | ||
require.JSONEq(t, string(tt.want), string(got)) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters