diff --git a/go.mod b/go.mod index 43c67bbb2..6900d7582 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index c449aba6d..71e6f3ec0 100644 --- a/go.sum +++ b/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= diff --git a/pkg/auth/apikey.go b/pkg/auth/apikey.go index 9e153f11b..edba6e44b 100644 --- a/pkg/auth/apikey.go +++ b/pkg/auth/apikey.go @@ -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 -} diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go deleted file mode 100644 index c3c7d2875..000000000 --- a/pkg/auth/auth.go +++ /dev/null @@ -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") diff --git a/pkg/auth/grpcauth/apikey.go b/pkg/auth/grpcauth/apikey.go new file mode 100644 index 000000000..aa44065b6 --- /dev/null +++ b/pkg/auth/grpcauth/apikey.go @@ -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 +} diff --git a/pkg/auth/grpcauth/apikey_test.go b/pkg/auth/grpcauth/apikey_test.go new file mode 100644 index 000000000..15e23652b --- /dev/null +++ b/pkg/auth/grpcauth/apikey_test.go @@ -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 +} diff --git a/pkg/auth/grpcauth/authentication.go b/pkg/auth/grpcauth/authentication.go deleted file mode 100644 index b37efb0b0..000000000 --- a/pkg/auth/grpcauth/authentication.go +++ /dev/null @@ -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...) - } -} diff --git a/pkg/auth/grpcauth/authentication_test.go b/pkg/auth/grpcauth/authentication_test.go deleted file mode 100644 index e6941d8eb..000000000 --- a/pkg/auth/grpcauth/authentication_test.go +++ /dev/null @@ -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"], "")) - } -} diff --git a/uplink/metainfo/client.go b/uplink/metainfo/client.go index 002bc4ce5..1e40fe7ff 100644 --- a/uplink/metainfo/client.go +++ b/uplink/metainfo/client.go @@ -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)