Skip to content

Commit

Permalink
sdk: Fix decision log masking of input object
Browse files Browse the repository at this point in the history
The sdk package allows you to pass arbitrary Go objects as the input
document. This is fine for evaluation (as long as the object can be
parsed to an AST), but is problematic for decision log masking, as the
masking logic expects the input on the event to be either a
map[string]interface{} or a []interface{}, and for inner types of the
object to also be similarly generic. Currently, the decision log masking
silently fails if the input object is not of the correct type.

This patch addresses this by rebuilding the input Go type for the
decision log from the parsed AST of the original input object. This
generates a Go type that does not break the masking logic, provided the
input is of a type that can be masked. The conversion from the AST only
happens if the decision logs plugin is actually registered with the
manager to avoid wasting cycles if decision logs are not enabled.

A test is added to cover the masking case for the SDK.

Signed-off-by: Erik Paulson <epaulson10@gmail.com>
  • Loading branch information
epaulson10 committed Jul 14, 2023
1 parent cda3bfb commit 939a0f8
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 0 deletions.
11 changes: 11 additions & 0 deletions sdk/opa.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,17 @@ func (opa *OPA) executeTransaction(ctx context.Context, record *server.Info, wor
record.Metrics.Timer(metrics.SDKDecisionEval).Stop()

if logger := logs.Lookup(s.manager); logger != nil {
// Decision log masking requires the event object to be a map[string]interface{},
// or a []interface{}, and all internal objects referenced in the mask to be
// similarly generic. Convert the input AST back into a JSON-representation to
// ensure decision logging will work if the input Go type does not fit these requirements.
if record.InputAST != nil {
asJSON, err := ast.JSON(record.InputAST)
if err != nil {
return nil, err
}
*record.Input = asJSON
}
if err := logger.Log(ctx, record); err != nil {
return result, fmt.Errorf("decision log: %w", err)
}
Expand Down
107 changes: 107 additions & 0 deletions sdk/opa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,113 @@ main = time.now_ns()

}

func TestDecisionLoggingWithMasking(t *testing.T) {

ctx := context.Background()

server := sdktest.MustNewServer(
sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{
"main.rego": `
package system
main = true
str = "foo"
loopback = input
`,
"log.rego": `
package system.log
mask["/input/secret"]
mask["/input/top/secret"]
mask["/input/dossier/1/highly"]
`,
}),
)

defer server.Stop()

config := fmt.Sprintf(`{
"services": {
"test": {
"url": %q
}
},
"bundles": {
"test": {
"resource": "/bundles/bundle.tar.gz"
}
},
"decision_logs": {
"console": true
}
}`, server.URL())

testLogger := loggingtest.New()
opa, err := sdk.New(ctx, sdk.Options{
Config: strings.NewReader(config),
ConsoleLogger: testLogger,
})
if err != nil {
t.Fatal(err)
}

defer opa.Stop(ctx)

if _, err := opa.Decision(ctx, sdk.DecisionOptions{
Input: map[string]interface{}{
"secret": "foo",
"top": map[string]string{
"secret": "bar",
},
"dossier": []map[string]interface{}{
{
"very": "private",
},
{
"highly": "classified",
},
},
},
}); err != nil {
t.Fatal(err)
}

entries := testLogger.Entries()

if len(entries) != 1 {
t.Fatalf("expected 1 entry but got %d", len(entries))
}

expectedErased := []interface{}{
"/input/dossier/1/highly",
"/input/secret",
"/input/top/secret",
}
erased := entries[0].Fields["erased"].([]interface{})
stringLess := func(a, b string) bool {
return a < b
}
if !cmp.Equal(expectedErased, erased, cmpopts.SortSlices(stringLess)) {
t.Errorf("Did not get expected result for erased field in decision log:\n%s", cmp.Diff(expectedErased, erased, cmpopts.SortSlices(stringLess)))
}
errMsg := `Expected masked field "%s" to be removed, but it was present.`
input := entries[0].Fields["input"].(map[string]interface{})
if _, ok := input["secret"]; ok {
t.Errorf(errMsg, "/input/secret")
}

if _, ok := input["top"].(map[string]interface{})["secret"]; ok {
t.Errorf(errMsg, "/input/top/secret")
}

if _, ok := input["dossier"].([]interface{})[1].(map[string]interface{})["highly"]; ok {
t.Errorf(errMsg, "/input/dossier/1/highly")
}

}

func TestDecisionLoggingWithNDBCache(t *testing.T) {

ctx := context.Background()
Expand Down

0 comments on commit 939a0f8

Please sign in to comment.