diff --git a/satellite/api.go b/satellite/api.go index 26c9b8c3d..629f2ce72 100644 --- a/satellite/api.go +++ b/satellite/api.go @@ -490,6 +490,33 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, }) } + { // setup userinfo. + if config.Userinfo.Enabled { + + peer.Userinfo.Endpoint, err = userinfo.NewEndpoint( + peer.Log.Named("userinfo:endpoint"), + peer.DB.Console().Users(), + peer.DB.Console().APIKeys(), + peer.DB.Console().Projects(), + config.Userinfo, + ) + if err != nil { + return nil, errs.Combine(err, peer.Close()) + } + + if err := pb.DRPCRegisterUserInfo(peer.Server.DRPC(), peer.Userinfo.Endpoint); err != nil { + return nil, errs.Combine(err, peer.Close()) + } + + peer.Services.Add(lifecycle.Item{ + Name: "userinfo:endpoint", + Close: peer.Userinfo.Endpoint.Close, + }) + } else { + peer.Log.Named("userinfo:endpoint").Info("disabled") + } + } + { // setup inspector peer.Inspector.Endpoint = inspector.NewEndpoint( peer.Log.Named("inspector"), diff --git a/satellite/console/userinfo/endpoint.go b/satellite/console/userinfo/endpoint.go index e1dee146d..5b08beca0 100644 --- a/satellite/console/userinfo/endpoint.go +++ b/satellite/console/userinfo/endpoint.go @@ -11,6 +11,7 @@ import ( "go.uber.org/zap" "storj.io/common/identity" + "storj.io/common/macaroon" "storj.io/common/pb" "storj.io/common/rpc/rpcpeer" "storj.io/common/rpc/rpcstatus" @@ -26,6 +27,7 @@ var ( // Config holds Endpoint's configuration. type Config struct { + Enabled bool `help:"Whether the private Userinfo rpc endpoint is enabled" default:"false"` AllowedPeers storj.NodeURLs `help:"A comma delimited list of peers (IDs/addresses) allowed to use this endpoint."` } @@ -67,7 +69,7 @@ func NewEndpoint(log *zap.Logger, users console.Users, apiKeys console.APIKeys, func (e *Endpoint) Close() error { return nil } // Get returns relevant info about the current user. -func (e *Endpoint) Get(ctx context.Context, _ *pb.GetUserInfoRequest) (response *pb.GetUserInfoResponse, err error) { +func (e *Endpoint) Get(ctx context.Context, req *pb.GetUserInfoRequest) (response *pb.GetUserInfoResponse, err error) { defer mon.Task()(&ctx)(&err) peer, err := rpcpeer.FromContext(ctx) @@ -84,9 +86,29 @@ func (e *Endpoint) Get(ctx context.Context, _ *pb.GetUserInfoRequest) (response return nil, rpcstatus.Error(rpcstatus.PermissionDenied, err.Error()) } - // TODO: implement get user info + key, err := e.getAPIKey(ctx, req.Header) + if err != nil { + return nil, rpcstatus.Error(rpcstatus.InvalidArgument, err.Error()) + } - return nil, rpcstatus.Error(rpcstatus.Unimplemented, "Get Userinfo not implemented") + info, err := e.apiKeys.GetByHead(ctx, key.Head()) + if err != nil { + return nil, rpcstatus.Error(rpcstatus.InvalidArgument, Error.Wrap(err).Error()) + } + + project, err := e.projects.Get(ctx, info.ProjectID) + if err != nil { + return nil, rpcstatus.Error(rpcstatus.NotFound, Error.Wrap(err).Error()) + } + + user, err := e.users.Get(ctx, project.OwnerID) + if err != nil { + return nil, rpcstatus.Error(rpcstatus.NotFound, Error.Wrap(err).Error()) + } + + return &pb.GetUserInfoResponse{ + PaidTier: user.PaidTier, + }, nil } // verifyPeer verifies that a peer is allowed. @@ -97,3 +119,20 @@ func (e *Endpoint) verifyPeer(id storj.NodeID) error { } return nil } + +func (e *Endpoint) getAPIKey(ctx context.Context, header *pb.RequestHeader) (key *macaroon.APIKey, err error) { + defer mon.Task()(&ctx)(&err) + + if header == nil { + return nil, Error.New("Missing API credentials") + } + + key, err = macaroon.ParseRawAPIKey(header.ApiKey) + if err != nil { + err = Error.Wrap(err) + e.log.Debug("Invalid credentials", zap.Error(err)) + return nil, err + } + + return key, nil +} diff --git a/satellite/console/userinfo/endpoint_test.go b/satellite/console/userinfo/endpoint_test.go index dbdb4316c..4f489cb2f 100644 --- a/satellite/console/userinfo/endpoint_test.go +++ b/satellite/console/userinfo/endpoint_test.go @@ -7,23 +7,24 @@ import ( "context" "crypto/tls" "crypto/x509" - "fmt" "testing" "github.com/stretchr/testify/require" "go.uber.org/zap" "storj.io/common/identity/testidentity" + "storj.io/common/macaroon" "storj.io/common/pb" "storj.io/common/rpc/rpcpeer" + "storj.io/common/rpc/rpcstatus" "storj.io/common/storj" "storj.io/common/testcontext" "storj.io/storj/private/testplanet" "storj.io/storj/satellite" + "storj.io/storj/satellite/console" ) -func TestEndpointGet_UnTrusted(t *testing.T) { - t.Skip("disable until UserInfo is added to API. See issue #5363") +func TestEndpointGet(t *testing.T) { // trusted identity ident, err := testidentity.NewTestIdentity(context.TODO()) @@ -35,38 +36,112 @@ func TestEndpointGet_UnTrusted(t *testing.T) { Satellite: func(log *zap.Logger, index int, config *satellite.Config) { url, err := storj.ParseNodeURL(ident.ID.String() + "@") require.NoError(t, err) + + config.Userinfo.Enabled = true config.Userinfo.AllowedPeers = storj.NodeURLs{url} }, }, }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { sat := planet.Satellites[0] + endpoint := sat.Userinfo.Endpoint + addr := sat.API.Console.Listener.Addr() - state := tls.ConnectionState{ - PeerCertificates: []*x509.Certificate{ident.Leaf, ident.CA}, - } - peerCtx := rpcpeer.NewContext(ctx, &rpcpeer.Peer{ - Addr: sat.API.Console.Listener.Addr(), - State: state, + t.Run("reject untrusted peer", func(t *testing.T) { + // untrusted identity. + badIdent, err := testidentity.NewTestIdentity(ctx) + require.NoError(t, err) + + state := tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{badIdent.Leaf, badIdent.CA}, + } + peerCtx := rpcpeer.NewContext(ctx, &rpcpeer.Peer{ + Addr: addr, + State: state, + }) + _, err = endpoint.Get(peerCtx, &pb.GetUserInfoRequest{}) + // an untrusted peer shouldn't be able to get Userinfo. + require.Error(t, err) + require.Equal(t, rpcstatus.PermissionDenied, rpcstatus.Code(err)) }) - _, err = sat.Userinfo.Endpoint.Get(peerCtx, &pb.GetUserInfoRequest{}) - // a trusted peer should be able to get Userinfo - require.NoError(t, err) - // untrusted identity - badIdent, err := testidentity.NewTestIdentity(ctx) - require.NoError(t, err) - - state = tls.ConnectionState{ - PeerCertificates: []*x509.Certificate{badIdent.Leaf, badIdent.CA}, - } - peerCtx = rpcpeer.NewContext(ctx, &rpcpeer.Peer{ - Addr: sat.API.Console.Listener.Addr(), - State: state, + t.Run("allow trusted peer", func(t *testing.T) { + // using trusted ident. + state := tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{ident.Leaf, ident.CA}, + } + peerCtx := rpcpeer.NewContext(ctx, &rpcpeer.Peer{ + Addr: addr, + State: state, + }) + _, err = endpoint.Get(peerCtx, &pb.GetUserInfoRequest{}) + // trusted peer should not get an untrusted error + // but an error for not adding API key to the request. + require.Error(t, err) + require.Equal(t, rpcstatus.InvalidArgument, rpcstatus.Code(err)) + }) + + t.Run("get userinfo", func(t *testing.T) { + newUser := console.CreateUser{ + FullName: "username", + ShortName: "", + Email: "userinfo@test.test", + } + + user, err := sat.AddUser(ctx, newUser, 1) + require.NoError(t, err) + require.Equal(t, false, user.PaidTier) + + project, err := sat.AddProject(ctx, user.ID, "info") + require.NoError(t, err) + + secret, err := macaroon.NewSecret() + require.NoError(t, err) + + key, err := macaroon.NewAPIKey(secret) + require.NoError(t, err) + + keyInfo := console.APIKeyInfo{ + Name: "test", + ProjectID: project.ID, + Secret: secret, + } + + _, err = sat.DB.Console().APIKeys().Create(ctx, key.Head(), keyInfo) + require.NoError(t, err) + + state := tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{ident.Leaf, ident.CA}, + } + peerCtx := rpcpeer.NewContext(ctx, &rpcpeer.Peer{ + Addr: addr, + State: state, + }) + + response, err := endpoint.Get(peerCtx, &pb.GetUserInfoRequest{ + Header: &pb.RequestHeader{ + ApiKey: key.SerializeRaw(), + }, + }) + // a trusted peer should be able to get Userinfo. + require.NoError(t, err) + require.Equal(t, false, response.PaidTier) + + userCtx, err := sat.UserContext(ctx, user.ID) + require.NoError(t, err) + // add a credit card to put the user in the paid tier. + err = sat.API.Console.Service.Payments().AddCreditCard(userCtx, "test-cc-token") + require.NoError(t, err) + + // get user info again + response, err = endpoint.Get(peerCtx, &pb.GetUserInfoRequest{ + Header: &pb.RequestHeader{ + ApiKey: key.SerializeRaw(), + }, + }) + require.NoError(t, err) + // user should now be in paid tier. + require.Equal(t, true, response.PaidTier) }) - _, err = sat.Userinfo.Endpoint.Get(peerCtx, &pb.GetUserInfoRequest{}) - // an untrusted peer shouldn't be able to get Userinfo - require.Error(t, err) - require.EqualError(t, err, fmt.Sprintf("userinfo_endpoint: peer %q is untrusted", badIdent.ID)) }) } diff --git a/scripts/testdata/satellite-config.yaml.lock b/scripts/testdata/satellite-config.yaml.lock index fbe03e9f4..cab1b0290 100755 --- a/scripts/testdata/satellite-config.yaml.lock +++ b/scripts/testdata/satellite-config.yaml.lock @@ -1066,6 +1066,9 @@ server.private-address: 127.0.0.1:7778 # A comma delimited list of peers (IDs/addresses) allowed to use this endpoint. # userinfo.allowed-peers: "" +# Whether the private Userinfo rpc endpoint is enabled +# userinfo.enabled: false + # Interval to check the version # version.check-interval: 15m0s