diff options
| author | mo khan <mo@mokhan.ca> | 2025-05-20 14:28:06 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-05-23 14:49:19 -0600 |
| commit | 4beee46dc6c7642316e118a4d3aa51e4b407256e (patch) | |
| tree | 039bdf57b99061844aeb0fe55ad0bc1c864166af /vendor/github.com/fullstorydev/grpcurl/format.go | |
| parent | 0ba49bfbde242920d8675a193d7af89420456fc0 (diff) | |
feat: add external authorization service (authzd) with JWT authentication
- Add new authzd gRPC service implementing Envoy's external authorization API
- Integrate JWT authentication filter in Envoy configuration with claim extraction
- Update middleware to support both cookie-based and header-based user authentication
- Add comprehensive test coverage for authorization service and server
- Configure proper service orchestration with authzd, sparkled, and Envoy
- Update build system and Docker configuration for multi-service deployment
- Add grpcurl tool for gRPC service debugging and testing
This enables fine-grained authorization control through Envoy's ext_authz filter
while maintaining backward compatibility with existing cookie-based authentication.
Diffstat (limited to 'vendor/github.com/fullstorydev/grpcurl/format.go')
| -rw-r--r-- | vendor/github.com/fullstorydev/grpcurl/format.go | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/vendor/github.com/fullstorydev/grpcurl/format.go b/vendor/github.com/fullstorydev/grpcurl/format.go new file mode 100644 index 0000000..e7f576b --- /dev/null +++ b/vendor/github.com/fullstorydev/grpcurl/format.go @@ -0,0 +1,554 @@ +package grpcurl + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "reflect" + "strings" + "sync" + + "github.com/golang/protobuf/jsonpb" //lint:ignore SA1019 we have to import these because some of their types appear in exported API + "github.com/golang/protobuf/proto" //lint:ignore SA1019 same as above + "github.com/jhump/protoreflect/desc" //lint:ignore SA1019 same as above + "github.com/jhump/protoreflect/dynamic" //lint:ignore SA1019 same as above + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// RequestParser processes input into messages. +type RequestParser interface { + // Next parses input data into the given request message. If called after + // input is exhausted, it returns io.EOF. If the caller re-uses the same + // instance in multiple calls to Next, it should call msg.Reset() in between + // each call. + Next(msg proto.Message) error + // NumRequests returns the number of messages that have been parsed and + // returned by a call to Next. + NumRequests() int +} + +type jsonRequestParser struct { + dec *json.Decoder + unmarshaler jsonpb.Unmarshaler + requestCount int +} + +// NewJSONRequestParser returns a RequestParser that reads data in JSON format +// from the given reader. The given resolver is used to assist with decoding of +// google.protobuf.Any messages. +// +// Input data that contains more than one message should just include all +// messages concatenated (though whitespace is necessary to separate some kinds +// of values in JSON). +// +// If the given reader has no data, the returned parser will return io.EOF on +// the very first call. +func NewJSONRequestParser(in io.Reader, resolver jsonpb.AnyResolver) RequestParser { + return &jsonRequestParser{ + dec: json.NewDecoder(in), + unmarshaler: jsonpb.Unmarshaler{AnyResolver: resolver}, + } +} + +// NewJSONRequestParserWithUnmarshaler is like NewJSONRequestParser but +// accepts a protobuf jsonpb.Unmarshaler instead of jsonpb.AnyResolver. +func NewJSONRequestParserWithUnmarshaler(in io.Reader, unmarshaler jsonpb.Unmarshaler) RequestParser { + return &jsonRequestParser{ + dec: json.NewDecoder(in), + unmarshaler: unmarshaler, + } +} + +func (f *jsonRequestParser) Next(m proto.Message) error { + var msg json.RawMessage + if err := f.dec.Decode(&msg); err != nil { + return err + } + f.requestCount++ + return f.unmarshaler.Unmarshal(bytes.NewReader(msg), m) +} + +func (f *jsonRequestParser) NumRequests() int { + return f.requestCount +} + +const ( + textSeparatorChar = '\x1e' +) + +type textRequestParser struct { + r *bufio.Reader + err error + requestCount int +} + +// NewTextRequestParser returns a RequestParser that reads data in the protobuf +// text format from the given reader. +// +// Input data that contains more than one message should include an ASCII +// 'Record Separator' character (0x1E) between each message. +// +// Empty text is a valid text format and represents an empty message. So if the +// given reader has no data, the returned parser will yield an empty message +// for the first call to Next and then return io.EOF thereafter. This also means +// that if the input data ends with a record separator, then a final empty +// message will be parsed *after* the separator. +func NewTextRequestParser(in io.Reader) RequestParser { + return &textRequestParser{r: bufio.NewReader(in)} +} + +func (f *textRequestParser) Next(m proto.Message) error { + if f.err != nil { + return f.err + } + + var b []byte + b, f.err = f.r.ReadBytes(textSeparatorChar) + if f.err != nil && f.err != io.EOF { + return f.err + } + // remove delimiter + if len(b) > 0 && b[len(b)-1] == textSeparatorChar { + b = b[:len(b)-1] + } + + f.requestCount++ + + return proto.UnmarshalText(string(b), m) +} + +func (f *textRequestParser) NumRequests() int { + return f.requestCount +} + +// Formatter translates messages into string representations. +type Formatter func(proto.Message) (string, error) + +// NewJSONFormatter returns a formatter that returns JSON strings. The JSON will +// include empty/default values (instead of just omitted them) if emitDefaults +// is true. The given resolver is used to assist with encoding of +// google.protobuf.Any messages. +func NewJSONFormatter(emitDefaults bool, resolver jsonpb.AnyResolver) Formatter { + marshaler := jsonpb.Marshaler{ + EmitDefaults: emitDefaults, + AnyResolver: resolver, + } + // Workaround for indentation issue in jsonpb with Any messages. + // Bug was originally fixed in https://github.com/golang/protobuf/pull/834 + // but later re-introduced before the module was deprecated and frozen. + // If jsonpb is ever replaced with google.golang.org/protobuf/encoding/protojson + // this workaround will no longer be needed. + formatter := func(message proto.Message) (string, error) { + output, err := marshaler.MarshalToString(message) + if err != nil { + return "", err + } + var buf bytes.Buffer + if err := json.Indent(&buf, []byte(output), "", " "); err != nil { + return "", err + } + return buf.String(), nil + } + return formatter +} + +// NewTextFormatter returns a formatter that returns strings in the protobuf +// text format. If includeSeparator is true then, when invoked to format +// multiple messages, all messages after the first one will be prefixed with the +// ASCII 'Record Separator' character (0x1E). +func NewTextFormatter(includeSeparator bool) Formatter { + tf := textFormatter{useSeparator: includeSeparator} + return tf.format +} + +type textFormatter struct { + useSeparator bool + numFormatted int +} + +var protoTextMarshaler = proto.TextMarshaler{ExpandAny: true} + +func (tf *textFormatter) format(m proto.Message) (string, error) { + var buf bytes.Buffer + if tf.useSeparator && tf.numFormatted > 0 { + if err := buf.WriteByte(textSeparatorChar); err != nil { + return "", err + } + } + + // If message implements MarshalText method (such as a *dynamic.Message), + // it won't get details about whether or not to format to text compactly + // or with indentation. So first see if the message also implements a + // MarshalTextIndent method and use that instead if available. + type indentMarshaler interface { + MarshalTextIndent() ([]byte, error) + } + + if indenter, ok := m.(indentMarshaler); ok { + b, err := indenter.MarshalTextIndent() + if err != nil { + return "", err + } + if _, err := buf.Write(b); err != nil { + return "", err + } + } else if err := protoTextMarshaler.Marshal(&buf, m); err != nil { + return "", err + } + + // no trailing newline needed + str := buf.String() + if len(str) > 0 && str[len(str)-1] == '\n' { + str = str[:len(str)-1] + } + + tf.numFormatted++ + + return str, nil +} + +// Format of request data. The allowed values are 'json' or 'text'. +type Format string + +const ( + // FormatJSON specifies input data in JSON format. Multiple request values + // may be concatenated (messages with a JSON representation other than + // object must be separated by whitespace, such as a newline) + FormatJSON = Format("json") + + // FormatText specifies input data must be in the protobuf text format. + // Multiple request values must be separated by the "record separator" + // ASCII character: 0x1E. The stream should not end in a record separator. + // If it does, it will be interpreted as a final, blank message after the + // separator. + FormatText = Format("text") +) + +// AnyResolverFromDescriptorSource returns an AnyResolver that will search for +// types using the given descriptor source. +func AnyResolverFromDescriptorSource(source DescriptorSource) jsonpb.AnyResolver { + return &anyResolver{source: source} +} + +// AnyResolverFromDescriptorSourceWithFallback returns an AnyResolver that will +// search for types using the given descriptor source and then fallback to a +// special message if the type is not found. The fallback type will render to +// JSON with a "@type" property, just like an Any message, but also with a +// custom "@value" property that includes the binary encoded payload. +func AnyResolverFromDescriptorSourceWithFallback(source DescriptorSource) jsonpb.AnyResolver { + res := anyResolver{source: source} + return &anyResolverWithFallback{AnyResolver: &res} +} + +type anyResolver struct { + source DescriptorSource + + er dynamic.ExtensionRegistry + + mu sync.RWMutex + mf *dynamic.MessageFactory + resolved map[string]func() proto.Message +} + +func (r *anyResolver) Resolve(typeUrl string) (proto.Message, error) { + mname := typeUrl + if slash := strings.LastIndex(mname, "/"); slash >= 0 { + mname = mname[slash+1:] + } + + r.mu.RLock() + factory := r.resolved[mname] + r.mu.RUnlock() + + // already resolved? + if factory != nil { + return factory(), nil + } + + r.mu.Lock() + defer r.mu.Unlock() + + // double-check, in case we were racing with another goroutine + // that resolved this one + factory = r.resolved[mname] + if factory != nil { + return factory(), nil + } + + // use descriptor source to resolve message type + d, err := r.source.FindSymbol(mname) + if err != nil { + return nil, err + } + md, ok := d.(*desc.MessageDescriptor) + if !ok { + return nil, fmt.Errorf("unknown message: %s", typeUrl) + } + // populate any extensions for this message, too (if there are any) + if exts, err := r.source.AllExtensionsForType(mname); err == nil { + if err := r.er.AddExtension(exts...); err != nil { + return nil, err + } + } + + if r.mf == nil { + r.mf = dynamic.NewMessageFactoryWithExtensionRegistry(&r.er) + } + + factory = func() proto.Message { + return r.mf.NewMessage(md) + } + if r.resolved == nil { + r.resolved = map[string]func() proto.Message{} + } + r.resolved[mname] = factory + return factory(), nil +} + +// anyResolverWithFallback can provide a fallback value for unknown +// messages that will format itself to JSON using an "@value" field +// that has the base64-encoded data for the unknown message value. +type anyResolverWithFallback struct { + jsonpb.AnyResolver +} + +func (r anyResolverWithFallback) Resolve(typeUrl string) (proto.Message, error) { + msg, err := r.AnyResolver.Resolve(typeUrl) + if err == nil { + return msg, err + } + + // Try "default" resolution logic. This mirrors the default behavior + // of jsonpb, which checks to see if the given message name is registered + // in the proto package. + mname := typeUrl + if slash := strings.LastIndex(mname, "/"); slash >= 0 { + mname = mname[slash+1:] + } + //lint:ignore SA1019 new non-deprecated API requires other code changes; deferring... + mt := proto.MessageType(mname) + if mt != nil { + return reflect.New(mt.Elem()).Interface().(proto.Message), nil + } + + // finally, fallback to a special placeholder that can marshal itself + // to JSON using a special "@value" property to show base64-encoded + // data for the embedded message + return &unknownAny{TypeUrl: typeUrl, Error: fmt.Sprintf("%s is not recognized; see @value for raw binary message data", mname)}, nil +} + +type unknownAny struct { + TypeUrl string `json:"@type"` + Error string `json:"@error"` + Value string `json:"@value"` +} + +func (a *unknownAny) MarshalJSONPB(jsm *jsonpb.Marshaler) ([]byte, error) { + if jsm.Indent != "" { + return json.MarshalIndent(a, "", jsm.Indent) + } + return json.Marshal(a) +} + +func (a *unknownAny) Unmarshal(b []byte) error { + a.Value = base64.StdEncoding.EncodeToString(b) + return nil +} + +func (a *unknownAny) Reset() { + a.Value = "" +} + +func (a *unknownAny) String() string { + b, err := a.MarshalJSONPB(&jsonpb.Marshaler{}) + if err != nil { + return fmt.Sprintf("ERROR: %v", err.Error()) + } + return string(b) +} + +func (a *unknownAny) ProtoMessage() { +} + +var _ proto.Message = (*unknownAny)(nil) + +// FormatOptions is a set of flags that are passed to a JSON or text formatter. +type FormatOptions struct { + // EmitJSONDefaultFields flag, when true, includes empty/default values in the output. + // FormatJSON only flag. + EmitJSONDefaultFields bool + + // AllowUnknownFields is an option for the parser. When true, + // it accepts input which includes unknown fields. These unknown fields + // are skipped instead of returning an error. + // FormatJSON only flag. + AllowUnknownFields bool + + // IncludeTextSeparator is true then, when invoked to format multiple messages, + // all messages after the first one will be prefixed with the + // ASCII 'Record Separator' character (0x1E). + // It might be useful when the output is piped to another grpcurl process. + // FormatText only flag. + IncludeTextSeparator bool +} + +// RequestParserAndFormatter returns a request parser and formatter for the +// given format. The given descriptor source may be used for parsing message +// data (if needed by the format). +// It accepts a set of options. The field EmitJSONDefaultFields and IncludeTextSeparator +// are options for JSON and protobuf text formats, respectively. The AllowUnknownFields field +// is a JSON-only format flag. +// Requests will be parsed from the given in. +func RequestParserAndFormatter(format Format, descSource DescriptorSource, in io.Reader, opts FormatOptions) (RequestParser, Formatter, error) { + switch format { + case FormatJSON: + resolver := AnyResolverFromDescriptorSource(descSource) + unmarshaler := jsonpb.Unmarshaler{AnyResolver: resolver, AllowUnknownFields: opts.AllowUnknownFields} + return NewJSONRequestParserWithUnmarshaler(in, unmarshaler), NewJSONFormatter(opts.EmitJSONDefaultFields, anyResolverWithFallback{AnyResolver: resolver}), nil + case FormatText: + return NewTextRequestParser(in), NewTextFormatter(opts.IncludeTextSeparator), nil + default: + return nil, nil, fmt.Errorf("unknown format: %s", format) + } +} + +// RequestParserAndFormatterFor returns a request parser and formatter for the +// given format. The given descriptor source may be used for parsing message +// data (if needed by the format). The flags emitJSONDefaultFields and +// includeTextSeparator are options for JSON and protobuf text formats, +// respectively. Requests will be parsed from the given in. +// This function is deprecated. Please use RequestParserAndFormatter instead. +// DEPRECATED +func RequestParserAndFormatterFor(format Format, descSource DescriptorSource, emitJSONDefaultFields, includeTextSeparator bool, in io.Reader) (RequestParser, Formatter, error) { + return RequestParserAndFormatter(format, descSource, in, FormatOptions{ + EmitJSONDefaultFields: emitJSONDefaultFields, + IncludeTextSeparator: includeTextSeparator, + }) +} + +// DefaultEventHandler logs events to a writer. This is not thread-safe, but is +// safe for use with InvokeRPC as long as NumResponses and Status are not read +// until the call to InvokeRPC completes. +type DefaultEventHandler struct { + Out io.Writer + Formatter Formatter + // 0 = default + // 1 = verbose + // 2 = very verbose + VerbosityLevel int + + // NumResponses is the number of responses that have been received. + NumResponses int + // Status is the status that was received at the end of an RPC. It is + // nil if the RPC is still in progress. + Status *status.Status +} + +// NewDefaultEventHandler returns an InvocationEventHandler that logs events to +// the given output. If verbose is true, all events are logged. Otherwise, only +// response messages are logged. +// +// Deprecated: NewDefaultEventHandler exists for compatibility. +// It doesn't allow fine control over the `VerbosityLevel` +// and provides only 0 and 1 options (which corresponds to the `verbose` argument). +// Use DefaultEventHandler{} initializer directly. +func NewDefaultEventHandler(out io.Writer, descSource DescriptorSource, formatter Formatter, verbose bool) *DefaultEventHandler { + verbosityLevel := 0 + if verbose { + verbosityLevel = 1 + } + return &DefaultEventHandler{ + Out: out, + Formatter: formatter, + VerbosityLevel: verbosityLevel, + } +} + +var _ InvocationEventHandler = (*DefaultEventHandler)(nil) + +func (h *DefaultEventHandler) OnResolveMethod(md *desc.MethodDescriptor) { + if h.VerbosityLevel > 0 { + txt, err := GetDescriptorText(md, nil) + if err == nil { + fmt.Fprintf(h.Out, "\nResolved method descriptor:\n%s\n", txt) + } + } +} + +func (h *DefaultEventHandler) OnSendHeaders(md metadata.MD) { + if h.VerbosityLevel > 0 { + fmt.Fprintf(h.Out, "\nRequest metadata to send:\n%s\n", MetadataToString(md)) + } +} + +func (h *DefaultEventHandler) OnReceiveHeaders(md metadata.MD) { + if h.VerbosityLevel > 0 { + fmt.Fprintf(h.Out, "\nResponse headers received:\n%s\n", MetadataToString(md)) + } +} + +func (h *DefaultEventHandler) OnReceiveResponse(resp proto.Message) { + h.NumResponses++ + if h.VerbosityLevel > 1 { + fmt.Fprintf(h.Out, "\nEstimated response size: %d bytes\n", proto.Size(resp)) + } + if h.VerbosityLevel > 0 { + fmt.Fprint(h.Out, "\nResponse contents:\n") + } + if respStr, err := h.Formatter(resp); err != nil { + fmt.Fprintf(h.Out, "Failed to format response message %d: %v\n", h.NumResponses, err) + } else { + fmt.Fprintln(h.Out, respStr) + } +} + +func (h *DefaultEventHandler) OnReceiveTrailers(stat *status.Status, md metadata.MD) { + h.Status = stat + if h.VerbosityLevel > 0 { + fmt.Fprintf(h.Out, "\nResponse trailers received:\n%s\n", MetadataToString(md)) + } +} + +// PrintStatus prints details about the given status to the given writer. The given +// formatter is used to print any detail messages that may be included in the status. +// If the given status has a code of OK, "OK" is printed and that is all. Otherwise, +// "ERROR:" is printed along with a line showing the code, one showing the message +// string, and each detail message if any are present. The detail messages will be +// printed as proto text format or JSON, depending on the given formatter. +func PrintStatus(w io.Writer, stat *status.Status, formatter Formatter) { + if stat.Code() == codes.OK { + fmt.Fprintln(w, "OK") + return + } + fmt.Fprintf(w, "ERROR:\n Code: %s\n Message: %s\n", stat.Code().String(), stat.Message()) + + statpb := stat.Proto() + if len(statpb.Details) > 0 { + fmt.Fprintf(w, " Details:\n") + for i, det := range statpb.Details { + prefix := fmt.Sprintf(" %d)", i+1) + fmt.Fprintf(w, "%s\t", prefix) + prefix = strings.Repeat(" ", len(prefix)) + "\t" + + output, err := formatter(det) + if err != nil { + fmt.Fprintf(w, "Error parsing detail message: %v\n", err) + } else { + lines := strings.Split(output, "\n") + for i, line := range lines { + if i == 0 { + // first line is already indented + fmt.Fprintf(w, "%s\n", line) + } else { + fmt.Fprintf(w, "%s%s\n", prefix, line) + } + } + } + } + } +} |
