-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cleanups and refactor of protosanitizer #184
Changes from all commits
b7eddd9
4b75450
36e3fcb
a085ea1
648bd9e
a4d492b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,12 +21,10 @@ package protosanitizer | |
import ( | ||
"encoding/json" | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
|
||
"github.com/golang/protobuf/descriptor" | ||
"github.com/golang/protobuf/proto" | ||
protobufdescriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" | ||
"github.com/container-storage-interface/spec/lib/go/csi" | ||
"google.golang.org/protobuf/proto" | ||
"google.golang.org/protobuf/reflect/protoreflect" | ||
) | ||
|
||
// StripSecrets returns a wrapper around the original CSI gRPC message | ||
|
@@ -42,135 +40,81 @@ import ( | |
// result to logging functions which may or may not end up serializing | ||
// the parameter depending on the current log level. | ||
func StripSecrets(msg interface{}) fmt.Stringer { | ||
return &stripSecrets{msg, isCSI1Secret} | ||
} | ||
|
||
// StripSecretsCSI03 is like StripSecrets, except that it works | ||
// for messages based on CSI 0.3 and older. It does not work | ||
// for CSI 1.0, use StripSecrets for that. | ||
func StripSecretsCSI03(msg interface{}) fmt.Stringer { | ||
return &stripSecrets{msg, isCSI03Secret} | ||
return &stripSecrets{msg} | ||
} | ||
|
||
type stripSecrets struct { | ||
msg interface{} | ||
|
||
isSecretField func(field *protobufdescriptor.FieldDescriptorProto) bool | ||
msg any | ||
} | ||
|
||
func (s *stripSecrets) String() string { | ||
// First convert to a generic representation. That's less efficient | ||
// than using reflect directly, but easier to work with. | ||
var parsed interface{} | ||
b, err := json.Marshal(s.msg) | ||
if err != nil { | ||
return fmt.Sprintf("<<json.Marshal %T: %s>>", s.msg, err) | ||
} | ||
if err := json.Unmarshal(b, &parsed); err != nil { | ||
return fmt.Sprintf("<<json.Unmarshal %T: %s>>", s.msg, err) | ||
} | ||
stripped := s.msg | ||
|
||
// Now remove secrets from the generic representation of the message. | ||
s.strip(parsed, s.msg) | ||
// also support scalar types like string, int, etc. | ||
msg, ok := s.msg.(proto.Message) | ||
if ok { | ||
stripped = stripMessage(msg.ProtoReflect()) | ||
} | ||
|
||
// Re-encoded the stripped representation and return that. | ||
b, err = json.Marshal(parsed) | ||
b, err := json.Marshal(stripped) | ||
if err != nil { | ||
return fmt.Sprintf("<<json.Marshal %T: %s>>", s.msg, err) | ||
} | ||
return string(b) | ||
} | ||
|
||
func (s *stripSecrets) strip(parsed interface{}, msg interface{}) { | ||
protobufMsg, ok := msg.(descriptor.Message) | ||
if !ok { | ||
// Not a protobuf message, so we are done. | ||
return | ||
func stripSingleValue(field protoreflect.FieldDescriptor, v protoreflect.Value) any { | ||
switch field.Kind() { | ||
case protoreflect.MessageKind: | ||
return stripMessage(v.Message()) | ||
case protoreflect.EnumKind: | ||
return field.Enum().Values().ByNumber(v.Enum()).Name() | ||
default: | ||
return v.Interface() | ||
} | ||
} | ||
|
||
// The corresponding map in the parsed JSON representation. | ||
parsedFields, ok := parsed.(map[string]interface{}) | ||
if !ok { | ||
// Probably nil. | ||
return | ||
func stripValue(field protoreflect.FieldDescriptor, v protoreflect.Value) any { | ||
if field.IsList() { | ||
l := v.List() | ||
res := make([]any, l.Len()) | ||
for i := range l.Len() { | ||
res[i] = stripSingleValue(field, l.Get(i)) | ||
Comment on lines
+81
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For example, what happens when a list has a list? Will it call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. List is represented as repeated field in the protobuf. Like:
And it cannot be nested without going through a level of message. i.e.:
is not valid, but this one is:
Similarly, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the explanation! |
||
} | ||
return res | ||
} else if field.IsMap() { | ||
m := v.Map() | ||
res := make(map[string]any, m.Len()) | ||
m.Range(func(mk protoreflect.MapKey, v protoreflect.Value) bool { | ||
res[mk.String()] = stripSingleValue(field.MapValue(), v) | ||
return true | ||
}) | ||
return res | ||
} else { | ||
return stripSingleValue(field, v) | ||
} | ||
} | ||
|
||
func stripMessage(msg protoreflect.Message) map[string]any { | ||
stripped := make(map[string]any) | ||
|
||
// Walk through all fields and replace those with ***stripped*** that | ||
// are marked as secret. This relies on protobuf adding "json:" tags | ||
// on each field where the name matches the field name in the protobuf | ||
// spec (like volume_capabilities). The field.GetJsonName() method returns | ||
// a different name (volumeCapabilities) which we don't use. | ||
_, md := descriptor.ForMessage(protobufMsg) | ||
fields := md.GetField() | ||
if fields != nil { | ||
for _, field := range fields { | ||
if s.isSecretField(field) { | ||
// Overwrite only if already set. | ||
if _, ok := parsedFields[field.GetName()]; ok { | ||
parsedFields[field.GetName()] = "***stripped***" | ||
} | ||
} else if field.GetType() == protobufdescriptor.FieldDescriptorProto_TYPE_MESSAGE { | ||
// When we get here, | ||
// the type name is something like ".csi.v1.CapacityRange" (leading dot!) | ||
// and looking up "csi.v1.CapacityRange" | ||
// returns the type of a pointer to a pointer | ||
// to CapacityRange. We need a pointer to such | ||
// a value for recursive stripping. | ||
typeName := field.GetTypeName() | ||
if strings.HasPrefix(typeName, ".") { | ||
typeName = typeName[1:] | ||
} | ||
t := proto.MessageType(typeName) | ||
if t == nil || t.Kind() != reflect.Ptr { | ||
// Shouldn't happen, but | ||
// better check anyway instead | ||
// of panicking. | ||
continue | ||
} | ||
v := reflect.New(t.Elem()) | ||
|
||
// Recursively strip the message(s) that | ||
// the field contains. | ||
i := v.Interface() | ||
entry := parsedFields[field.GetName()] | ||
if slice, ok := entry.([]interface{}); ok { | ||
// Array of values, like VolumeCapabilities in CreateVolumeRequest. | ||
for _, entry := range slice { | ||
s.strip(entry, i) | ||
} | ||
} else { | ||
// Single value. | ||
s.strip(entry, i) | ||
} | ||
} | ||
// are marked as secret. | ||
msg.Range(func(field protoreflect.FieldDescriptor, v protoreflect.Value) bool { | ||
name := field.TextName() | ||
if isCSI1Secret(field) { | ||
stripped[name] = "***stripped***" | ||
} else { | ||
stripped[name] = stripValue(field, v) | ||
} | ||
} | ||
return true | ||
}) | ||
return stripped | ||
} | ||
|
||
// isCSI1Secret uses the csi.E_CsiSecret extension from CSI 1.0 to | ||
// determine whether a field contains secrets. | ||
func isCSI1Secret(field *protobufdescriptor.FieldDescriptorProto) bool { | ||
ex, err := proto.GetExtension(field.Options, e_CsiSecret) | ||
return err == nil && ex != nil && *ex.(*bool) | ||
} | ||
|
||
// Copied from the CSI 1.0 spec (https://github.com/container-storage-interface/spec/blob/37e74064635d27c8e33537c863b37ccb1182d4f8/lib/go/csi/csi.pb.go#L4520-L4527) | ||
// to avoid a package dependency that would prevent usage of this package | ||
// in repos using an older version of the spec. | ||
// | ||
// Future revision of the CSI spec must not change this extensions, otherwise | ||
// they will break filtering in binaries based on the 1.0 version of the spec. | ||
var e_CsiSecret = &proto.ExtensionDesc{ | ||
ExtendedType: (*protobufdescriptor.FieldOptions)(nil), | ||
ExtensionType: (*bool)(nil), | ||
Field: 1059, | ||
Name: "csi.v1.csi_secret", | ||
Tag: "varint,1059,opt,name=csi_secret,json=csiSecret", | ||
Filename: "github.com/container-storage-interface/spec/csi.proto", | ||
} | ||
|
||
// isCSI03Secret relies on the naming convention in CSI <= 0.3 | ||
// to determine whether a field contains secrets. | ||
func isCSI03Secret(field *protobufdescriptor.FieldDescriptorProto) bool { | ||
return strings.HasSuffix(field.GetName(), "_secrets") | ||
func isCSI1Secret(desc protoreflect.FieldDescriptor) bool { | ||
ex := proto.GetExtension(desc.Options(), csi.E_CsiSecret) | ||
return ex.(bool) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be
stripValue()
andstripSingleValue()
combined into a single function with a bigger switch?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible, but less clear, I think. Because for field that
IsList()
, itsKind()
still returns the kind of list element. So, we will have multiple case statements matched.My code structure actually looks very like the official encoder:
https://github.com/protocolbuffers/protobuf-go/blob/b98563540c0a4edb38526bcd6e6c97f9fac1f453/encoding/prototext/encode.go#L201-L212
(I'm not referencing it when writing, but reached agreement eventually :)