2021-10-19 17:21:41 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2021-10-19 17:21:41 -04:00
package controller
import (
"context"
"fmt"
"net/http"
"reflect"
2021-12-16 15:13:28 -08:00
"runtime/debug"
2024-05-17 11:59:50 -04:00
"time"
2021-10-19 17:21:41 -04:00
2021-12-16 15:13:28 -08:00
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
2024-04-06 13:48:47 -07:00
"github.com/hashicorp/boundary/globals"
2024-02-13 09:17:41 -08:00
"github.com/hashicorp/boundary/internal/alias"
2022-05-09 12:45:10 -07:00
commonSrv "github.com/hashicorp/boundary/internal/daemon/common"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
2021-10-19 17:21:41 -04:00
"github.com/hashicorp/boundary/internal/errors"
2023-09-12 08:15:34 -04:00
"github.com/hashicorp/boundary/internal/event"
2021-10-19 17:21:41 -04:00
pb "github.com/hashicorp/boundary/internal/gen/controller/api"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
pberrors "github.com/hashicorp/boundary/internal/gen/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/requests"
2024-04-06 13:48:47 -07:00
"github.com/hashicorp/go-uuid"
2021-10-19 17:21:41 -04:00
"github.com/mr-tron/base58"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
const (
requestInfoMdKey = "request-info"
// domainErrHeader defines an http header for encoded domain errors from the
// grpc server.
domainErrHeader = "x-domain-err"
// apiErrHeader defines an http header for encoded api errors from the
// grpc server.
apiErrHeader = "x-api-err"
)
2023-05-20 07:23:10 -04:00
// customContextServerStream wraps the grpc.ServerStream interface and lets us
// set a custom context
type customContextServerStream struct {
grpc . ServerStream
customContext context . Context
}
func ( c * customContextServerStream ) Context () context . Context {
if c . customContext != nil {
return c . customContext
}
return c . ServerStream . Context ()
}
// requestCtxUnaryInterceptor creates an unary server interceptor that pulls
// grpc metadata into a ctx for the request. The metadata must be set in an
// upstream http handler/middleware by marshalling a RequestInfo protobuf into
// the requestInfoMdKey header (see: controller.wrapHandlerWithCommonFuncs).
func requestCtxUnaryInterceptor (
2021-10-19 17:21:41 -04:00
ctx context . Context ,
iamRepoFn common . IamRepoFactory ,
authTokenRepoFn common . AuthTokenRepoFactory ,
serversRepoFn common . ServersRepoFactory ,
2022-10-27 09:01:05 -04:00
passwordAuthRepoFn common . PasswordAuthRepoFactory ,
oidcAuthRepoFn common . OidcAuthRepoFactory ,
2023-03-15 11:40:40 -04:00
ldapAuthRepoFn common . LdapAuthRepoFactory ,
2021-10-19 17:21:41 -04:00
kms * kms . Kms ,
ticket string ,
2021-10-23 21:23:49 -04:00
eventer * event . Eventer ,
2021-10-19 17:21:41 -04:00
) ( grpc . UnaryServerInterceptor , error ) {
2023-05-20 07:23:10 -04:00
const op = "controller.requestCtxUnaryInterceptor"
if err := sharedRequestInterceptorValidation (
ctx ,
op ,
iamRepoFn ,
authTokenRepoFn ,
serversRepoFn ,
kms ,
ticket ,
eventer ,
); err != nil {
return nil , err
}
return func ( interceptorCtx context . Context ,
req any ,
_ * grpc . UnaryServerInfo ,
handler grpc . UnaryHandler ,
) ( any , error ) {
updatedCtx , err := sharedRequestInterceptorLogic (
interceptorCtx ,
op ,
iamRepoFn ,
authTokenRepoFn ,
serversRepoFn ,
passwordAuthRepoFn ,
oidcAuthRepoFn ,
ldapAuthRepoFn ,
kms ,
ticket ,
eventer ,
)
if err != nil {
return nil , err
}
return handler ( updatedCtx , req )
}, nil
}
// requestCtxStreamInterceptor creates a stream server interceptor that pulls
// grpc metadata into a ctx for the request. The metadata must be set in an
// upstream http handler/middleware by marshalling a RequestInfo protobuf into
// the requestInfoMdKey header (see: controller.wrapHandlerWithCommonFuncs).
func requestCtxStreamInterceptor (
ctx context . Context ,
iamRepoFn common . IamRepoFactory ,
authTokenRepoFn common . AuthTokenRepoFactory ,
serversRepoFn common . ServersRepoFactory ,
passwordAuthRepoFn common . PasswordAuthRepoFactory ,
oidcAuthRepoFn common . OidcAuthRepoFactory ,
ldapAuthRepoFn common . LdapAuthRepoFactory ,
kms * kms . Kms ,
ticket string ,
eventer * event . Eventer ,
) ( grpc . StreamServerInterceptor , error ) {
const op = "controller.requestCtxStreamInterceptor"
if err := sharedRequestInterceptorValidation (
ctx ,
op ,
iamRepoFn ,
authTokenRepoFn ,
serversRepoFn ,
kms ,
ticket ,
eventer ,
); err != nil {
return nil , err
}
return func ( srv any ,
ss grpc . ServerStream ,
info * grpc . StreamServerInfo ,
handler grpc . StreamHandler ,
) error {
updatedCtx , err := sharedRequestInterceptorLogic (
ss . Context (),
op ,
iamRepoFn ,
authTokenRepoFn ,
serversRepoFn ,
passwordAuthRepoFn ,
oidcAuthRepoFn ,
ldapAuthRepoFn ,
kms ,
ticket ,
eventer ,
)
if err != nil {
return err
}
css := & customContextServerStream {
ServerStream : ss ,
customContext : updatedCtx ,
}
return handler ( srv , css )
}, nil
}
func sharedRequestInterceptorValidation (
ctx context . Context ,
op errors . Op ,
iamRepoFn common . IamRepoFactory ,
authTokenRepoFn common . AuthTokenRepoFactory ,
serversRepoFn common . ServersRepoFactory ,
kms * kms . Kms ,
ticket string ,
eventer * event . Eventer ,
) error {
2021-10-19 17:21:41 -04:00
if iamRepoFn == nil {
2023-05-20 07:23:10 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing iam repo function" )
2021-10-19 17:21:41 -04:00
}
if authTokenRepoFn == nil {
2023-05-20 07:23:10 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing auth token repo function" )
2021-10-19 17:21:41 -04:00
}
if serversRepoFn == nil {
2023-05-20 07:23:10 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing server repo function" )
2021-10-19 17:21:41 -04:00
}
if kms == nil {
2023-05-20 07:23:10 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing kms" )
2021-10-19 17:21:41 -04:00
}
if ticket == "" {
2023-05-20 07:23:10 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing ticket" )
2021-10-19 17:21:41 -04:00
}
2021-10-23 21:23:49 -04:00
if eventer == nil {
2023-05-20 07:23:10 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing eventer" )
2021-10-23 21:23:49 -04:00
}
2021-10-19 17:21:41 -04:00
2023-05-20 07:23:10 -04:00
return nil
}
2021-10-19 17:21:41 -04:00
2023-05-20 07:23:10 -04:00
func sharedRequestInterceptorLogic (
interceptorCtx context . Context ,
op errors . Op ,
iamRepoFn common . IamRepoFactory ,
authTokenRepoFn common . AuthTokenRepoFactory ,
serversRepoFn common . ServersRepoFactory ,
passwordAuthRepoFn common . PasswordAuthRepoFactory ,
oidcAuthRepoFn common . OidcAuthRepoFactory ,
ldapAuthRepoFn common . LdapAuthRepoFactory ,
kms * kms . Kms ,
ticket string ,
eventer * event . Eventer ,
) ( context . Context , error ) {
md , ok := metadata . FromIncomingContext ( interceptorCtx )
if ! ok {
return nil , errors . New ( interceptorCtx , errors . Internal , op , "No metadata" )
}
2021-10-19 17:21:41 -04:00
2023-05-20 07:23:10 -04:00
values := md . Get ( requestInfoMdKey )
if len ( values ) == 0 {
return nil , errors . New ( interceptorCtx , errors . Internal , op , "Missing request metadata" )
}
if len ( values ) > 1 {
return nil , errors . New ( interceptorCtx , errors . Internal , op , fmt . Sprintf ( "expected 1 value for %s metadata and got %d" , requestInfoMdKey , len ( values )))
}
2021-10-23 21:23:49 -04:00
2023-05-20 07:23:10 -04:00
decoded , err := base58 . FastBase58Decoding ( values [ 0 ])
if err != nil {
return nil , errors . Wrap ( interceptorCtx , err , op , errors . WithCode ( errors . Internal ), errors . WithMsg ( "unable to decode request info" ))
}
var requestInfo authpb . RequestInfo
if err := proto . Unmarshal ( decoded , & requestInfo ); err != nil {
return nil , errors . Wrap ( interceptorCtx , err , op , errors . WithCode ( errors . Internal ), errors . WithMsg ( "unable to unmarshal request info" ))
}
switch {
case requestInfo . Ticket == "" :
return nil , errors . New ( interceptorCtx , errors . Internal , op , "Invalid context (missing ticket)" )
case requestInfo . Ticket != ticket :
return nil , errors . New ( interceptorCtx , errors . Internal , op , "Invalid context (bad ticket)" )
}
2021-10-19 17:21:41 -04:00
2023-05-20 07:23:10 -04:00
interceptorCtx = auth . NewVerifierContextWithAccounts ( interceptorCtx , iamRepoFn , authTokenRepoFn , serversRepoFn , passwordAuthRepoFn , oidcAuthRepoFn , ldapAuthRepoFn , kms , & requestInfo )
// Add general request information to the context. The information from
// the auth verifier context is pretty specifically curated to
// authentication/authorization verification so this is more
// general-purpose.
//
// We could use requests.NewRequestContext but this saves an immediate
// lookup.
interceptorCtx = context . WithValue ( interceptorCtx , requests . ContextRequestInformationKey , & requests . RequestContext {
Path : requestInfo . Path ,
Method : requestInfo . Method ,
})
// This event request info is required by downstream handlers
info := & event . RequestInfo {
EventId : requestInfo . EventId ,
Id : requestInfo . TraceId ,
PublicId : requestInfo . PublicId ,
Method : requestInfo . Method ,
Path : requestInfo . Path ,
ClientIp : requestInfo . ClientIp ,
}
interceptorCtx , err = event . NewRequestInfoContext ( interceptorCtx , info )
if err != nil {
return nil , errors . Wrap ( interceptorCtx , err , op , errors . WithCode ( errors . Internal ), errors . WithMsg ( "unable to create context with request info" ))
}
interceptorCtx , err = event . NewEventerContext ( interceptorCtx , eventer )
if err != nil {
return nil , errors . Wrap ( interceptorCtx , err , op , errors . WithCode ( errors . Internal ), errors . WithMsg ( "unable to create context with eventer" ))
}
return interceptorCtx , err // not convinced we want to wrap every error and turn them into domain errors...
2021-10-19 17:21:41 -04:00
}
2024-04-06 13:48:47 -07:00
func correlationIdInterceptor (
_ context . Context ,
) grpc . UnaryServerInterceptor {
const op = "controller.correlationIdInterceptor"
return func ( interceptorCtx context . Context , req any ,
_ * grpc . UnaryServerInfo , handler grpc . UnaryHandler ,
) ( any , error ) {
md , ok := metadata . FromIncomingContext ( interceptorCtx )
if ! ok {
return nil , errors . New ( interceptorCtx , errors . Internal , op , "no metadata" )
}
values := md . Get ( globals . CorrelationIdKey )
if len ( values ) == 0 {
return nil , errors . New ( interceptorCtx , errors . Internal , op , "missing correlation id metadata" )
}
if len ( values ) > 1 {
return nil , errors . New ( interceptorCtx , errors . Internal , op , fmt . Sprintf ( "expected 1 value for %s metadata and got %d" , globals . CorrelationIdKey , len ( values )))
}
correlationId := values [ 0 ]
// Validate the correlationId
if _ , err := uuid . ParseUUID ( correlationId ); err != nil {
return nil , errors . Wrap ( interceptorCtx , err , op , errors . WithMsg ( "failed to validated correlation id" ))
}
interceptorCtx , err := event . NewCorrelationIdContext ( interceptorCtx , correlationId )
if err != nil {
return nil , errors . Wrap ( interceptorCtx , err , op , errors . WithCode ( errors . Internal ), errors . WithMsg ( "unable to create context with correlation id" ))
}
// call the handler...
return handler ( interceptorCtx , req )
}
}
2021-10-19 17:21:41 -04:00
func errorInterceptor (
_ context . Context ,
) grpc . UnaryServerInterceptor {
const op = "controller.errorInterceptor"
return func ( interceptorCtx context . Context ,
2022-10-25 18:29:38 -05:00
req any ,
2021-10-19 17:21:41 -04:00
_ * grpc . UnaryServerInfo ,
2022-10-25 18:29:38 -05:00
handler grpc . UnaryHandler ) ( any , error ,
2022-01-05 14:38:38 -05:00
) {
2021-10-19 17:21:41 -04:00
// call the handler...
h , handlerErr := handler ( interceptorCtx , req )
// if there's an error and it's a domain err, then encode it into the
if handlerErr != nil {
var domainErr * errors . Err
isDomainErr := errors . As ( handlerErr , & domainErr )
if isDomainErr {
pbErr := errors . ToPbErrors ( domainErr )
var buf [] byte
var err error
if buf , err = proto . Marshal ( pbErr ); err != nil {
return h , status . Errorf ( codes . Internal , "unable to marshal domain error: %s" , err )
}
if err := grpc . SetHeader ( interceptorCtx , metadata . Pairs ( domainErrHeader , base58 . FastBase58Encoding ( buf ))); err != nil {
return h , status . Errorf ( codes . Internal , "unable to set domain error header: %s" , err )
}
return h , handlerErr
}
var apiErr * handlers . ApiError
isApiError := errors . As ( handlerErr , & apiErr )
if isApiError {
pbErr := & pberrors . ApiError {
ApiError : apiErr . Inner ,
Status : apiErr . Status ,
}
var buf [] byte
var err error
if buf , err = proto . Marshal ( pbErr ); err != nil {
return h , status . Errorf ( codes . Internal , "unable to marshal api error: %s" , err )
}
if err := grpc . SetHeader ( interceptorCtx , metadata . Pairs ( apiErrHeader , base58 . FastBase58Encoding ( buf ))); err != nil {
return h , status . Errorf ( codes . Internal , "unable to set api error header: %s" , err )
}
return h , handlerErr
}
}
return h , handlerErr
}
}
2024-02-13 09:17:41 -08:00
// aliasResolutionInterceptor returns a grpc.UnaryServerInterceptor that resolves
// alias values in the request to their corresponding destination ids. If no
// alias is found or the alias has no destination id, an error is returned.
// For an field in the request to be considered for alias resolution, it must
// be annotated with the Aliasable proto option.
func aliasResolutionInterceptor (
2024-03-27 16:14:49 -07:00
_ context . Context ,
2024-02-13 09:17:41 -08:00
aliasRepoFn common . AliasRepoFactory ,
) grpc . UnaryServerInterceptor {
return func ( interceptorCtx context . Context ,
req any ,
_ * grpc . UnaryServerInfo ,
handler grpc . UnaryHandler ) ( any , error ,
) {
reqMsg , ok := req .( proto . Message )
if ! ok {
return nil , handlers . InvalidArgumentErrorf ( "The request was not a proto.Message." , nil )
}
r , err := aliasRepoFn ()
if err != nil {
return nil , err
}
interceptorCtx , err = alias . ResolveRequestIds ( interceptorCtx , reqMsg , r )
if err != nil {
2024-03-27 16:14:49 -07:00
// Since this is intercepted prior to checking that the requester
// is authorized to make the request, returning a 404 here and a 401
// when the request is checked in the handler would expose which
// aliases exist. Instead, we return a unauthenticated error here so there
// is no way to distinguish between a missing alias and an existing
// alias that points to a destination the requester is not allowed
// to perform an action on.
// handlers.UnauthenticatedError() turns into an http 401 status
// code "Unauthorized".
return nil , handlers . UnauthenticatedError ()
2024-02-13 09:17:41 -08:00
}
return handler ( interceptorCtx , req )
}
}
2021-10-19 17:21:41 -04:00
func statusCodeInterceptor (
_ context . Context ,
) grpc . UnaryServerInterceptor {
const op = "controller.statusCodeInterceptor"
return func ( interceptorCtx context . Context ,
2022-10-25 18:29:38 -05:00
req any ,
2021-10-19 17:21:41 -04:00
_ * grpc . UnaryServerInfo ,
2022-10-25 18:29:38 -05:00
handler grpc . UnaryHandler ) ( any , error ,
2022-01-05 14:38:38 -05:00
) {
2021-10-19 17:21:41 -04:00
// call the handler...
h , handlerErr := handler ( interceptorCtx , req )
// if a service handler returns nil, nil then we want to single a 204
// response to the proxy with no resp msg
if isNil ( h ) && handlerErr == nil {
if err := handlers . SetStatusCode ( interceptorCtx , http . StatusNoContent ); err != nil {
return & pb . EmptyResponse {}, err
}
return & pb . EmptyResponse {}, nil
}
return h , handlerErr
}
}
2022-10-25 18:29:38 -05:00
func isNil ( i any ) bool {
2021-10-19 17:21:41 -04:00
if i == nil {
return true
}
switch reflect . TypeOf ( i ). Kind () {
case reflect . Ptr , reflect . Map , reflect . Array , reflect . Chan , reflect . Slice :
return reflect . ValueOf ( i ). IsNil ()
}
return false
}
2021-10-23 21:23:49 -04:00
2023-09-19 17:16:37 -07:00
func eventsRequestInterceptor (
2021-10-23 21:23:49 -04:00
_ context . Context ,
) grpc . UnaryServerInterceptor {
2023-09-19 17:16:37 -07:00
const op = "controller.eventsRequestInterceptor"
2021-10-23 21:23:49 -04:00
return func ( interceptorCtx context . Context ,
2022-10-25 18:29:38 -05:00
req any ,
2021-10-23 21:23:49 -04:00
_ * grpc . UnaryServerInfo ,
2022-10-25 18:29:38 -05:00
handler grpc . UnaryHandler ) ( any , error ,
2022-01-05 14:38:38 -05:00
) {
2021-10-23 21:23:49 -04:00
if msg , ok := req .( proto . Message ); ok {
2022-04-08 09:38:21 -04:00
// Clone the request before writing it to the audit log,
// in case downstream interceptors modify it.
clonedMsg := proto . Clone ( msg )
if err := event . WriteAudit ( interceptorCtx , op , event . WithRequest ( & event . Request { Details : clonedMsg })); err != nil {
2021-10-23 21:23:49 -04:00
return req , status . Errorf ( codes . Internal , "unable to write request msg audit: %s" , err )
}
2023-09-19 17:16:37 -07:00
if err := event . WriteObservation ( interceptorCtx , op , event . WithRequest ( & event . Request { Details : clonedMsg })); err != nil {
return req , status . Errorf ( codes . Internal , "unable to write request msg observation: %s" , err )
}
2021-10-23 21:23:49 -04:00
}
return handler ( interceptorCtx , req )
}
}
2023-09-19 17:16:37 -07:00
func eventsResponseInterceptor (
2021-10-23 21:23:49 -04:00
_ context . Context ,
) grpc . UnaryServerInterceptor {
2023-09-19 17:16:37 -07:00
const op = "controller.eventsResponseInterceptor"
2021-10-23 21:23:49 -04:00
return func ( interceptorCtx context . Context ,
2022-10-25 18:29:38 -05:00
req any ,
2021-10-23 21:23:49 -04:00
_ * grpc . UnaryServerInfo ,
2022-10-25 18:29:38 -05:00
handler grpc . UnaryHandler ) ( any , error ,
2022-01-05 14:38:38 -05:00
) {
2021-10-23 21:23:49 -04:00
// call the handler...
resp , err := handler ( interceptorCtx , req )
if msg , ok := resp .( proto . Message ); ok {
2022-04-08 09:38:21 -04:00
// Clone the response before writing it to the audit log,
// in case downstream interceptors modify it.
clonedMsg := proto . Clone ( msg )
if err := event . WriteAudit ( interceptorCtx , op , event . WithResponse ( & event . Response { Details : clonedMsg })); err != nil {
2021-10-23 21:23:49 -04:00
return req , status . Errorf ( codes . Internal , "unable to write response msg audit: %s" , err )
}
2023-09-19 17:16:37 -07:00
if err := event . WriteObservation ( interceptorCtx , op , event . WithResponse ( & event . Response { Details : clonedMsg })); err != nil {
return req , status . Errorf ( codes . Internal , "unable to write response msg observation: %s" , err )
}
2021-10-23 21:23:49 -04:00
}
return resp , err
}
}
2021-11-09 11:43:41 -05:00
2024-05-17 11:59:50 -04:00
func requestMaxDurationInterceptor ( _ context . Context , maxRequestDuration time . Duration ) grpc . UnaryServerInterceptor {
const op = "controller.requestMaxDurationInterceptor"
return func ( interceptorCtx context . Context , req any , _ * grpc . UnaryServerInfo , handler grpc . UnaryHandler ) ( any , error ) {
withTimeout , cancel := context . WithTimeout ( interceptorCtx , maxRequestDuration )
defer cancel ()
return handler ( withTimeout , req )
}
}
2021-11-09 11:43:41 -05:00
func workerRequestInfoInterceptor ( ctx context . Context , eventer * event . Eventer ) ( grpc . UnaryServerInterceptor , error ) {
const op = "worker.requestInfoInterceptor"
if eventer == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing eventer" )
}
return func ( interceptorCtx context . Context ,
2022-10-25 18:29:38 -05:00
req any ,
2021-11-09 11:43:41 -05:00
srvInfo * grpc . UnaryServerInfo ,
2022-04-19 11:42:54 -04:00
handler grpc . UnaryHandler ,
2022-10-25 18:29:38 -05:00
) ( any , error ) {
2021-11-09 11:43:41 -05:00
var err error
id , err := event . NewId ( event . IdPrefix )
if err != nil {
event . WriteError ( interceptorCtx , op , err , event . WithInfoMsg ( "unable to create id for event" , "method" , srvInfo . FullMethod ))
return nil , status . Errorf ( codes . Internal , "Error creating id for event: %v" , err )
}
info := & event . RequestInfo {
EventId : id ,
Id : commonSrv . GeneratedTraceId ( interceptorCtx ),
Method : srvInfo . FullMethod ,
}
interceptorCtx , err = event . NewRequestInfoContext ( interceptorCtx , info )
if err != nil {
event . WriteError ( interceptorCtx , op , err , event . WithInfoMsg ( "unable to create context with request info" , "method" , srvInfo . FullMethod ))
return nil , status . Errorf ( codes . Internal , "Error creating context with request info: %v" , err )
}
interceptorCtx , err = event . NewEventerContext ( interceptorCtx , eventer )
if err != nil {
event . WriteError ( interceptorCtx , op , err , event . WithInfoMsg ( "unable to create context with eventer" , "method" , srvInfo . FullMethod ))
return nil , status . Errorf ( codes . Internal , "Error creating context with eventer: %v" , err )
}
// call the handler...
return handler ( interceptorCtx , req )
}, nil
}
2021-12-16 15:13:28 -08:00
func recoveryHandler () grpc_recovery . RecoveryHandlerFuncContext {
const op = "controller.recoveryHandler"
2022-10-25 18:29:38 -05:00
return func ( ctx context . Context , p any ) ( err error ) {
2021-12-16 15:13:28 -08:00
event . WriteError (
ctx ,
op ,
fmt . Errorf ( "recovered from panic: %v" , p ),
event . WithInfo ( "stack" , string ( debug . Stack ())),
)
return status . Errorf ( codes . Internal , "%v" , p )
}
}