pkg/auth: use grpc.WithPerRPCCredentials (#2670)
This commit is contained in:
parent
9496a8adb1
commit
9ba8b53ed5
10
go.mod
10
go.mod
@ -39,7 +39,7 @@ require (
|
||||
github.com/gogo/protobuf v1.2.1
|
||||
github.com/golang-migrate/migrate/v3 v3.5.2
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/golang/protobuf v1.3.1
|
||||
github.com/golang/protobuf v1.3.2
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/go-cmp v0.3.0
|
||||
@ -115,15 +115,15 @@ require (
|
||||
go.uber.org/multierr v1.1.0 // indirect
|
||||
go.uber.org/zap v1.10.0
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb
|
||||
golang.org/x/sys v0.0.0-20190730183949-1393eb018365
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
golang.org/x/tools v0.0.0-20190614152001-1edc8e83c897
|
||||
google.golang.org/appengine v1.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df // indirect
|
||||
google.golang.org/grpc v1.22.0
|
||||
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 // indirect
|
||||
google.golang.org/grpc v1.22.1
|
||||
gopkg.in/Shopify/sarama.v1 v1.18.0 // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25 // indirect
|
||||
gopkg.in/olivere/elastic.v5 v5.0.76 // indirect
|
||||
|
10
go.sum
10
go.sum
@ -120,6 +120,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
@ -421,6 +423,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
@ -442,6 +446,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190730183949-1393eb018365 h1:SaXEMXhWzMJThc05vu6uh61Q245r4KaWMrsTedk0FDc=
|
||||
golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
@ -476,6 +482,8 @@ google.golang.org/genproto v0.0.0-20180912233945-5a2fd4cab2d6 h1:YN6g8chEdBTmaER
|
||||
google.golang.org/genproto v0.0.0-20180912233945-5a2fd4cab2d6/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df h1:k3DT34vxk64+4bD5x+fRy6U0SXxZehzUHRSYUJcKfII=
|
||||
google.golang.org/genproto v0.0.0-20190701230453-710ae3a149df/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 h1:Ygq9/SRJX9+dU0WCIICM8RkWvDw03lvB77hrhJnpxfU=
|
||||
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.15.0 h1:Az/KuahOM4NAidTEuJCv/RonAA7rYsTPkqXVjr+8OOw=
|
||||
google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
@ -483,6 +491,8 @@ google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=
|
||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
gopkg.in/Shopify/sarama.v1 v1.18.0 h1:f9aTXuIEFEjVvLG9p+kMSk01dMfFumHsySRk1okTdqU=
|
||||
gopkg.in/Shopify/sarama.v1 v1.18.0/go.mod h1:AxnvoaevB2nBjNK17cG61A3LleFcWFwVBHBt+cot4Oc=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
|
||||
|
@ -5,44 +5,19 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
|
||||
monkit "gopkg.in/spacemonkeygo/monkit.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
mon = monkit.Package()
|
||||
)
|
||||
|
||||
// The key type is unexported to prevent collisions with context keys defined in
|
||||
// other packages.
|
||||
type key int
|
||||
|
||||
// apiKey is the context key for the user API Key
|
||||
const apiKey key = 0
|
||||
type apikey struct{}
|
||||
|
||||
// WithAPIKey creates context with api key
|
||||
func WithAPIKey(ctx context.Context, key []byte) context.Context {
|
||||
return context.WithValue(ctx, apiKey, key)
|
||||
return context.WithValue(ctx, apikey{}, key)
|
||||
}
|
||||
|
||||
// GetAPIKey returns api key from context is exists
|
||||
func GetAPIKey(ctx context.Context) ([]byte, bool) {
|
||||
key, ok := ctx.Value(apiKey).([]byte)
|
||||
key, ok := ctx.Value(apikey{}).([]byte)
|
||||
return key, ok
|
||||
}
|
||||
|
||||
// ValidateAPIKey compares the context api key with the key passed in as an argument
|
||||
func ValidateAPIKey(ctx context.Context, actualKey []byte) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
expectedKey, ok := GetAPIKey(ctx)
|
||||
if !ok {
|
||||
return Error.New("Could not get api key from context")
|
||||
}
|
||||
|
||||
matches := 1 == subtle.ConstantTimeCompare(actualKey, expectedKey)
|
||||
if !matches {
|
||||
return Error.New("Invalid API credential")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/zeebo/errs"
|
||||
)
|
||||
|
||||
// Error is the default auth error class
|
||||
var Error = errs.Class("auth error")
|
56
pkg/auth/grpcauth/apikey.go
Normal file
56
pkg/auth/grpcauth/apikey.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package grpcauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"storj.io/storj/pkg/auth"
|
||||
)
|
||||
|
||||
// NewAPIKeyInterceptor creates instance of apikey interceptor
|
||||
func NewAPIKeyInterceptor() grpc.UnaryServerInterceptor {
|
||||
return InterceptAPIKey
|
||||
}
|
||||
|
||||
// InterceptAPIKey reads apikey from requests and puts the value into the context.
|
||||
func InterceptAPIKey(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
apikeys, ok := md["apikey"]
|
||||
if !ok || len(apikeys) == 0 {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
return handler(auth.WithAPIKey(ctx, []byte(apikeys[0])), req)
|
||||
}
|
||||
|
||||
// APIKeyCredentials implements grpc/credentials.PerRPCCredentials
|
||||
// for authenticating with the grpc server.
|
||||
type APIKeyCredentials struct {
|
||||
value string
|
||||
}
|
||||
|
||||
// NewAPIKeyCredentials returns a new APIKeyCredentials
|
||||
func NewAPIKeyCredentials(apikey string) *APIKeyCredentials {
|
||||
return &APIKeyCredentials{apikey}
|
||||
}
|
||||
|
||||
// GetRequestMetadata gets the current request metadata, refreshing tokens if required.
|
||||
func (creds *APIKeyCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
"apikey": creds.value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RequireTransportSecurity indicates whether the credentials requires transport security.
|
||||
func (creds *APIKeyCredentials) RequireTransportSecurity() bool {
|
||||
return false
|
||||
}
|
89
pkg/auth/grpcauth/apikey_test.go
Normal file
89
pkg/auth/grpcauth/apikey_test.go
Normal file
@ -0,0 +1,89 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package grpcauth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
pb "google.golang.org/grpc/examples/helloworld/helloworld"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"storj.io/storj/internal/errs2"
|
||||
"storj.io/storj/internal/testcontext"
|
||||
"storj.io/storj/pkg/auth"
|
||||
"storj.io/storj/pkg/auth/grpcauth"
|
||||
)
|
||||
|
||||
func TestAPIKey(t *testing.T) {
|
||||
ctx := testcontext.New(t)
|
||||
defer ctx.Cleanup()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
// listener is closed in server.Stop() internally
|
||||
|
||||
server := grpc.NewServer(
|
||||
grpc.UnaryInterceptor(grpcauth.InterceptAPIKey),
|
||||
)
|
||||
defer server.Stop()
|
||||
|
||||
pb.RegisterGreeterServer(server, &helloServer{})
|
||||
|
||||
ctx.Go(func() error {
|
||||
err := errs2.IgnoreCanceled(server.Serve(listener))
|
||||
t.Log(err)
|
||||
return err
|
||||
})
|
||||
|
||||
type testcase struct {
|
||||
apikey string
|
||||
expected codes.Code
|
||||
}
|
||||
|
||||
for _, test := range []testcase{
|
||||
{"", codes.Unauthenticated},
|
||||
{"wrong key", codes.Unauthenticated},
|
||||
{"good key", codes.OK},
|
||||
} {
|
||||
conn, err := grpc.DialContext(ctx, listener.Addr().String(),
|
||||
grpc.WithPerRPCCredentials(grpcauth.NewAPIKeyCredentials(test.apikey)),
|
||||
grpc.WithBlock(),
|
||||
grpc.WithInsecure(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := pb.NewGreeterClient(conn)
|
||||
response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Me"})
|
||||
|
||||
if test.expected == codes.OK {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, response.Message, "Hello Me")
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, status.Code(err), test.expected)
|
||||
}
|
||||
|
||||
require.NoError(t, conn.Close())
|
||||
}
|
||||
}
|
||||
|
||||
type helloServer struct{}
|
||||
|
||||
// SayHello implements helloworld.GreeterServer
|
||||
func (s *helloServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
|
||||
key, ok := auth.GetAPIKey(ctx)
|
||||
if !ok {
|
||||
return nil, grpc.Errorf(codes.Unauthenticated, "Invalid API credentials")
|
||||
}
|
||||
if string(key) != "good key" {
|
||||
return nil, grpc.Errorf(codes.Unauthenticated, "Invalid API credentials")
|
||||
}
|
||||
|
||||
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package grpcauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"storj.io/storj/pkg/auth"
|
||||
)
|
||||
|
||||
// NewAPIKeyInterceptor creates instance of apikey interceptor
|
||||
func NewAPIKeyInterceptor() grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{},
|
||||
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{},
|
||||
err error) {
|
||||
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
apikeys, ok := md["apikey"]
|
||||
if !ok || len(apikeys) == 0 {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
return handler(auth.WithAPIKey(ctx, []byte(apikeys[0])), req)
|
||||
}
|
||||
}
|
||||
|
||||
// NewAPIKeyInjector injects api key to grpc connection context
|
||||
func NewAPIKeyInjector(apiKey string, callOpts ...grpc.CallOption) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
opts = append(opts, callOpts...)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "apikey", apiKey)
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package grpcauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"storj.io/storj/pkg/auth"
|
||||
)
|
||||
|
||||
func TestAPIKeyInterceptor(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
APIKey string
|
||||
err error
|
||||
}{
|
||||
{"", status.Errorf(codes.Unauthenticated, "Invalid API credential")},
|
||||
{"good key", nil},
|
||||
{"wrong key", status.Errorf(codes.Unauthenticated, "Invalid API credential")},
|
||||
} {
|
||||
interceptor := NewAPIKeyInterceptor()
|
||||
|
||||
// mock for method handler
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
APIKey, ok := auth.GetAPIKey(ctx)
|
||||
if !ok || string(APIKey) != "good key" {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "Invalid API credential")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
ctx = metadata.NewIncomingContext(ctx, metadata.Pairs("apikey", tt.APIKey))
|
||||
info := &grpc.UnaryServerInfo{}
|
||||
|
||||
_, err := interceptor(ctx, nil, info, handler)
|
||||
|
||||
assert.Equal(t, err, tt.err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIKeyInjector(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
APIKey string
|
||||
err error
|
||||
}{
|
||||
{"abc123", nil},
|
||||
{"", nil},
|
||||
} {
|
||||
injector := NewAPIKeyInjector(tt.APIKey)
|
||||
|
||||
// mock for method invoker
|
||||
var outputCtx context.Context
|
||||
invoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
|
||||
outputCtx = ctx
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := injector(ctx, "/test.method", nil, nil, nil, invoker)
|
||||
|
||||
assert.Equal(t, err, tt.err)
|
||||
|
||||
md, ok := metadata.FromOutgoingContext(outputCtx)
|
||||
assert.Equal(t, true, ok)
|
||||
assert.Equal(t, tt.APIKey, strings.Join(md["apikey"], ""))
|
||||
}
|
||||
}
|
@ -49,12 +49,11 @@ func New(client pb.MetainfoClient) *Client {
|
||||
}
|
||||
|
||||
// Dial dials to metainfo endpoint with the specified api key.
|
||||
func Dial(ctx context.Context, tc transport.Client, address string, apiKey string) (*Client, error) {
|
||||
apiKeyInjector := grpcauth.NewAPIKeyInjector(apiKey)
|
||||
func Dial(ctx context.Context, tc transport.Client, address string, apikey string) (*Client, error) {
|
||||
conn, err := tc.DialAddress(
|
||||
ctx,
|
||||
address,
|
||||
grpc.WithUnaryInterceptor(apiKeyInjector),
|
||||
grpc.WithPerRPCCredentials(grpcauth.NewAPIKeyCredentials(apikey)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
|
Loading…
Reference in New Issue
Block a user