| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package plugin |
| |
| import ( |
| "context" |
| "errors" |
| "math" |
| "sync/atomic" |
| |
| log "github.com/hashicorp/go-hclog" |
| "github.com/hashicorp/go-plugin" |
| "github.com/hashicorp/vault/sdk/helper/pluginutil" |
| "github.com/hashicorp/vault/sdk/logical" |
| "github.com/hashicorp/vault/sdk/plugin/pb" |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/status" |
| ) |
| |
| var ( |
| ErrPluginShutdown = errors.New("plugin is shut down") |
| ErrClientInMetadataMode = errors.New("plugin client can not perform action while in metadata mode") |
| ) |
| |
| // Validate backendGRPCPluginClient satisfies the logical.Backend interface |
| var _ logical.Backend = &backendGRPCPluginClient{} |
| |
| // backendPluginClient implements logical.Backend and is the |
| // go-plugin client. |
| type backendGRPCPluginClient struct { |
| broker *plugin.GRPCBroker |
| client pb.BackendClient |
| versionClient logical.PluginVersionClient |
| metadataMode bool |
| |
| system logical.SystemView |
| logger log.Logger |
| |
| // This is used to signal to the Cleanup function that it can proceed |
| // because we have a defined server |
| cleanupCh chan struct{} |
| |
| // server is the grpc server used for serving storage and sysview requests. |
| server *atomic.Value |
| |
| doneCtx context.Context |
| } |
| |
| func (b *backendGRPCPluginClient) Initialize(ctx context.Context, _ *logical.InitializationRequest) error { |
| if b.metadataMode { |
| return nil |
| } |
| |
| ctx, cancel := context.WithCancel(ctx) |
| quitCh := pluginutil.CtxCancelIfCanceled(cancel, b.doneCtx) |
| defer close(quitCh) |
| defer cancel() |
| |
| reply, err := b.client.Initialize(ctx, &pb.InitializeArgs{}, largeMsgGRPCCallOpts...) |
| if err != nil { |
| if b.doneCtx.Err() != nil { |
| return ErrPluginShutdown |
| } |
| |
| // If the plugin doesn't have Initialize implemented we should not fail |
| // the initialize call; otherwise this could halt startup of vault. |
| grpcStatus, ok := status.FromError(err) |
| if ok && grpcStatus.Code() == codes.Unimplemented { |
| return nil |
| } |
| |
| return err |
| } |
| if reply.Err != nil { |
| return pb.ProtoErrToErr(reply.Err) |
| } |
| |
| return nil |
| } |
| |
| func (b *backendGRPCPluginClient) HandleRequest(ctx context.Context, req *logical.Request) (*logical.Response, error) { |
| if b.metadataMode { |
| return nil, ErrClientInMetadataMode |
| } |
| |
| ctx, cancel := context.WithCancel(ctx) |
| quitCh := pluginutil.CtxCancelIfCanceled(cancel, b.doneCtx) |
| defer close(quitCh) |
| defer cancel() |
| |
| protoReq, err := pb.LogicalRequestToProtoRequest(req) |
| if err != nil { |
| return nil, err |
| } |
| |
| reply, err := b.client.HandleRequest(ctx, &pb.HandleRequestArgs{ |
| Request: protoReq, |
| }, largeMsgGRPCCallOpts...) |
| if err != nil { |
| if b.doneCtx.Err() != nil { |
| return nil, ErrPluginShutdown |
| } |
| |
| return nil, err |
| } |
| resp, err := pb.ProtoResponseToLogicalResponse(reply.Response) |
| if err != nil { |
| return nil, err |
| } |
| if reply.Err != nil { |
| return resp, pb.ProtoErrToErr(reply.Err) |
| } |
| |
| return resp, nil |
| } |
| |
| func (b *backendGRPCPluginClient) SpecialPaths() *logical.Paths { |
| reply, err := b.client.SpecialPaths(b.doneCtx, &pb.Empty{}) |
| if err != nil { |
| return nil |
| } |
| |
| if reply.Paths == nil { |
| return nil |
| } |
| |
| return &logical.Paths{ |
| Root: reply.Paths.Root, |
| Unauthenticated: reply.Paths.Unauthenticated, |
| LocalStorage: reply.Paths.LocalStorage, |
| SealWrapStorage: reply.Paths.SealWrapStorage, |
| WriteForwardedStorage: reply.Paths.WriteForwardedStorage, |
| } |
| } |
| |
| // System returns vault's system view. The backend client stores the view during |
| // Setup, so there is no need to shim the system just to get it back. |
| func (b *backendGRPCPluginClient) System() logical.SystemView { |
| return b.system |
| } |
| |
| // Logger returns vault's logger. The backend client stores the logger during |
| // Setup, so there is no need to shim the logger just to get it back. |
| func (b *backendGRPCPluginClient) Logger() log.Logger { |
| return b.logger |
| } |
| |
| func (b *backendGRPCPluginClient) HandleExistenceCheck(ctx context.Context, req *logical.Request) (bool, bool, error) { |
| if b.metadataMode { |
| return false, false, ErrClientInMetadataMode |
| } |
| |
| protoReq, err := pb.LogicalRequestToProtoRequest(req) |
| if err != nil { |
| return false, false, err |
| } |
| |
| ctx, cancel := context.WithCancel(ctx) |
| quitCh := pluginutil.CtxCancelIfCanceled(cancel, b.doneCtx) |
| defer close(quitCh) |
| defer cancel() |
| reply, err := b.client.HandleExistenceCheck(ctx, &pb.HandleExistenceCheckArgs{ |
| Request: protoReq, |
| }, largeMsgGRPCCallOpts...) |
| if err != nil { |
| if b.doneCtx.Err() != nil { |
| return false, false, ErrPluginShutdown |
| } |
| return false, false, err |
| } |
| if reply.Err != nil { |
| return false, false, pb.ProtoErrToErr(reply.Err) |
| } |
| |
| return reply.CheckFound, reply.Exists, nil |
| } |
| |
| func (b *backendGRPCPluginClient) Cleanup(ctx context.Context) { |
| ctx, cancel := context.WithCancel(ctx) |
| quitCh := pluginutil.CtxCancelIfCanceled(cancel, b.doneCtx) |
| defer close(quitCh) |
| defer cancel() |
| |
| b.client.Cleanup(ctx, &pb.Empty{}) |
| |
| // This will block until Setup has run the function to create a new server |
| // in b.server. If we stop here before it has a chance to actually start |
| // listening, when it starts listening it will immediately error out and |
| // exit, which is fine. Overall this ensures that we do not miss stopping |
| // the server if it ends up being created after Cleanup is called. |
| <-b.cleanupCh |
| server := b.server.Load() |
| if server != nil { |
| server.(*grpc.Server).GracefulStop() |
| } |
| } |
| |
| func (b *backendGRPCPluginClient) InvalidateKey(ctx context.Context, key string) { |
| if b.metadataMode { |
| return |
| } |
| |
| ctx, cancel := context.WithCancel(ctx) |
| quitCh := pluginutil.CtxCancelIfCanceled(cancel, b.doneCtx) |
| defer close(quitCh) |
| defer cancel() |
| |
| b.client.InvalidateKey(ctx, &pb.InvalidateKeyArgs{ |
| Key: key, |
| }) |
| } |
| |
| func (b *backendGRPCPluginClient) Setup(ctx context.Context, config *logical.BackendConfig) error { |
| // Shim logical.Storage |
| storageImpl := config.StorageView |
| if b.metadataMode { |
| storageImpl = &NOOPStorage{} |
| } |
| storage := &GRPCStorageServer{ |
| impl: storageImpl, |
| } |
| |
| // Shim logical.SystemView |
| sysViewImpl := config.System |
| if b.metadataMode { |
| sysViewImpl = &logical.StaticSystemView{} |
| } |
| sysView := &gRPCSystemViewServer{ |
| impl: sysViewImpl, |
| } |
| |
| events := &GRPCEventsServer{ |
| impl: config.EventsSender, |
| } |
| |
| // Register the server in this closure. |
| serverFunc := func(opts []grpc.ServerOption) *grpc.Server { |
| opts = append(opts, grpc.MaxRecvMsgSize(math.MaxInt32)) |
| opts = append(opts, grpc.MaxSendMsgSize(math.MaxInt32)) |
| |
| s := grpc.NewServer(opts...) |
| pb.RegisterSystemViewServer(s, sysView) |
| pb.RegisterStorageServer(s, storage) |
| pb.RegisterEventsServer(s, events) |
| b.server.Store(s) |
| close(b.cleanupCh) |
| return s |
| } |
| brokerID := b.broker.NextId() |
| go b.broker.AcceptAndServe(brokerID, serverFunc) |
| |
| args := &pb.SetupArgs{ |
| BrokerID: brokerID, |
| Config: config.Config, |
| BackendUUID: config.BackendUUID, |
| } |
| |
| ctx, cancel := context.WithCancel(ctx) |
| quitCh := pluginutil.CtxCancelIfCanceled(cancel, b.doneCtx) |
| defer close(quitCh) |
| defer cancel() |
| |
| reply, err := b.client.Setup(ctx, args) |
| if err != nil { |
| return err |
| } |
| if reply.Err != "" { |
| return errors.New(reply.Err) |
| } |
| |
| // Set system and logger for getter methods |
| b.system = config.System |
| b.logger = config.Logger |
| |
| return nil |
| } |
| |
| func (b *backendGRPCPluginClient) Type() logical.BackendType { |
| reply, err := b.client.Type(b.doneCtx, &pb.Empty{}) |
| if err != nil { |
| return logical.TypeUnknown |
| } |
| |
| return logical.BackendType(reply.Type) |
| } |
| |
| func (b *backendGRPCPluginClient) PluginVersion() logical.PluginVersion { |
| reply, err := b.versionClient.Version(b.doneCtx, &logical.Empty{}) |
| if err != nil { |
| if stErr, ok := status.FromError(err); ok { |
| if stErr.Code() == codes.Unimplemented { |
| return logical.EmptyPluginVersion |
| } |
| } |
| b.Logger().Warn("Unknown error getting plugin version", "err", err) |
| return logical.EmptyPluginVersion |
| } |
| return logical.PluginVersion{ |
| Version: reply.GetPluginVersion(), |
| } |
| } |
| |
| func (b *backendGRPCPluginClient) IsExternal() bool { |
| return true |
| } |