-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Redact http header values in logs (#343)
- Loading branch information
1 parent
04285e2
commit 0f9d7d7
Showing
8 changed files
with
315 additions
and
52 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,131 @@ | ||
/* Copyright (c) 2023 Snowflake Inc. All rights reserved. | ||
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 rpcauth | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
|
||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
"google.golang.org/protobuf/encoding/protojson" | ||
"google.golang.org/protobuf/reflect/protoreflect" | ||
"google.golang.org/protobuf/reflect/protoregistry" | ||
"google.golang.org/protobuf/types/descriptorpb" | ||
) | ||
|
||
func isMessage(descriptor protoreflect.FieldDescriptor) bool { | ||
return descriptor.Kind() == protoreflect.MessageKind || descriptor.Kind() == protoreflect.GroupKind | ||
} | ||
|
||
func isDebugRedactEnabled(fd protoreflect.FieldDescriptor) bool { | ||
opts, ok := fd.Options().(*descriptorpb.FieldOptions) | ||
if !ok { | ||
return false | ||
} | ||
return opts.GetDebugRedact() | ||
} | ||
|
||
func redactListField(value protoreflect.Value) { | ||
for i := 0; i < value.List().Len(); i++ { | ||
redactFields(value.List().Get(i).Message()) | ||
} | ||
} | ||
|
||
func redactMapField(value protoreflect.Value) { | ||
value.Map().Range(func(mapKey protoreflect.MapKey, mapValue protoreflect.Value) bool { | ||
redactFields(mapValue.Message()) | ||
return true | ||
}) | ||
} | ||
|
||
func redactNestedMessage(message protoreflect.Message, descriptor protoreflect.FieldDescriptor, value protoreflect.Value) { | ||
switch { | ||
case descriptor.IsList() && isMessage(descriptor): | ||
redactListField(value) | ||
case descriptor.IsMap() && isMessage(descriptor): | ||
redactMapField(value) | ||
case !descriptor.IsMap() && isMessage(descriptor): | ||
redactFields(value.Message()) | ||
} | ||
} | ||
|
||
func redactSingleField(message protoreflect.Message, descriptor protoreflect.FieldDescriptor) { | ||
if descriptor.Kind() == protoreflect.StringKind { | ||
if descriptor.Cardinality() != protoreflect.Repeated { | ||
message.Set(descriptor, protoreflect.ValueOfString("--REDACTED--")) | ||
} else { | ||
list := message.Mutable(descriptor).List() | ||
for i := 0; i < list.Len(); i++ { | ||
list.Set(i, protoreflect.ValueOfString("--REDACTED--")) | ||
} | ||
} | ||
} else { | ||
// other than string, clear it | ||
message.Clear(descriptor) | ||
} | ||
} | ||
|
||
func redactFields(message protoreflect.Message) { | ||
message.Range( | ||
func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool { | ||
if isDebugRedactEnabled(descriptor) { | ||
redactSingleField(message, descriptor) | ||
return true | ||
} | ||
redactNestedMessage(message, descriptor, value) | ||
return true | ||
}, | ||
) | ||
} | ||
|
||
func getRedactedInput(input *RPCAuthInput) (RPCAuthInput, error) { | ||
if input == nil { | ||
return RPCAuthInput{}, nil | ||
} | ||
redactedInput := RPCAuthInput{ | ||
Method: input.Method, | ||
MessageType: input.MessageType, | ||
Metadata: input.Metadata, | ||
Peer: input.Peer, | ||
Host: input.Host, | ||
Environment: input.Environment, | ||
Extensions: input.Extensions, | ||
} | ||
if input.MessageType == "" { | ||
return redactedInput, nil | ||
} | ||
var redactedMessage protoreflect.ProtoMessage | ||
if input != nil { | ||
// Transform the rpcauth input into the original proto | ||
messageType, err := protoregistry.GlobalTypes.FindMessageByURL(input.MessageType) | ||
if err != nil { | ||
return RPCAuthInput{}, fmt.Errorf("unable to find proto type %v: %v", input.MessageType, err) | ||
} | ||
redactedMessage = messageType.New().Interface() | ||
if err := protojson.Unmarshal([]byte(input.Message), redactedMessage); err != nil { | ||
return RPCAuthInput{}, fmt.Errorf("could not marshal input into %v: %v", input.MessageType, err) | ||
} | ||
redactFields(redactedMessage.ProtoReflect()) | ||
} | ||
marshaled, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(redactedMessage) | ||
if err != nil { | ||
return RPCAuthInput{}, status.Errorf(codes.Internal, "error marshalling request for auth: %v", err) | ||
} | ||
redactedInput.Message = json.RawMessage(marshaled) | ||
return redactedInput, nil | ||
} |
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,106 @@ | ||
/* Copyright (c) 2023 Snowflake Inc. All rights reserved. | ||
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 rpcauth | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
httppb "github.com/Snowflake-Labs/sansshell/services/httpoverrpc" | ||
"github.com/stretchr/testify/assert" | ||
"google.golang.org/protobuf/encoding/protojson" | ||
"google.golang.org/protobuf/reflect/protoregistry" | ||
) | ||
|
||
func TestGetRedactedInput(t *testing.T) { | ||
httpReq := httppb.HostHTTPRequest{ | ||
Port: 8080, | ||
Hostname: "localhost", | ||
Protocol: "https", | ||
Request: &httppb.HTTPRequest{ | ||
Method: "POST", | ||
RequestUri: "/", | ||
Headers: []*httppb.Header{ | ||
{Key: "key0", Values: []string{"val0"}}, | ||
}, | ||
}, | ||
} | ||
mockInput, _ := NewRPCAuthInput(context.TODO(), "/HTTPOverRPC.HTTPOverRPC/Host", httpReq.ProtoReflect().Interface()) | ||
|
||
for _, tc := range []struct { | ||
name string | ||
createInputFn func() *RPCAuthInput | ||
assertionFn func(RPCAuthInput) | ||
errFunc func(*testing.T, error) | ||
}{ | ||
{ | ||
name: "redacted fields should be redacted", | ||
createInputFn: func() *RPCAuthInput { | ||
return mockInput | ||
}, | ||
assertionFn: func(result RPCAuthInput) { | ||
messageType, _ := protoregistry.GlobalTypes.FindMessageByURL(mockInput.MessageType) | ||
resultMessage := messageType.New().Interface() | ||
err := protojson.Unmarshal([]byte(result.Message), resultMessage) | ||
assert.NoError(t, err) | ||
|
||
req := resultMessage.(*httppb.HostHTTPRequest) | ||
|
||
assert.Equal(t, "--REDACTED--", req.Request.Headers[0].Values[0]) // field with debug_redact should be redacted | ||
assert.Equal(t, "key0", req.Request.Headers[0].Key) // field without debug_redact should not be redacted | ||
}, | ||
errFunc: func(t *testing.T, err error) { | ||
assert.NoError(t, err) | ||
}, | ||
}, | ||
{ | ||
name: "malformed input should return err", | ||
createInputFn: func() *RPCAuthInput { | ||
i := &RPCAuthInput{ | ||
MessageType: "malformed", | ||
} | ||
return i | ||
}, | ||
errFunc: func(t *testing.T, err error) { | ||
assert.NotNil(t, err) | ||
}, | ||
}, | ||
{ | ||
name: "nil input should return nil", | ||
createInputFn: func() *RPCAuthInput { | ||
return nil | ||
}, | ||
assertionFn: func(i RPCAuthInput) { | ||
assert.Equal(t, RPCAuthInput{}, i) | ||
}, | ||
errFunc: func(t *testing.T, err error) { | ||
assert.NoError(t, err) | ||
}, | ||
}, | ||
} { | ||
t.Run(tc.name, func(t *testing.T) { | ||
input := tc.createInputFn() | ||
result, err := getRedactedInput(input) | ||
if tc.assertionFn != nil { | ||
tc.assertionFn(result) | ||
} | ||
if tc.errFunc != nil { | ||
tc.errFunc(t, err) | ||
} | ||
}) | ||
} | ||
} |
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
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
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
Oops, something went wrong.