-
Notifications
You must be signed in to change notification settings - Fork 54
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
Open
huww98
wants to merge
6
commits into
kubernetes-csi:master
Choose a base branch
from
huww98:update-proto
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b7eddd9
update csi spec and protoc
huww98 4b75450
fix warnings
huww98 36e3fcb
protosanitizer: rewrite with protoreflect
huww98 a085ea1
get rid of remaining github.com/golang/protobuf
huww98 648bd9e
protosanitizer: add benchmark
huww98 a4d492b
protosanitizer: recurse into all fields
huww98 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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 |
||
} | ||
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) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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?