pkg/auth: use grpc.WithPerRPCCredentials (#2670)

This commit is contained in:
Egon Elbre 2019-07-31 14:57:13 +03:00 committed by Michal Niewrzal
parent 9496a8adb1
commit 9ba8b53ed5
9 changed files with 165 additions and 165 deletions

10
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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
}

View File

@ -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")

View 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
}

View 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
}

View File

@ -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...)
}
}

View File

@ -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"], ""))
}
}

View File

@ -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)