Merge pull request #58 from filecoin-project/feat/apiauth

API Security
This commit is contained in:
Łukasz Magiera 2019-07-24 00:52:28 +02:00 committed by GitHub
commit c394e3c2aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 394 additions and 59 deletions

View File

@ -33,6 +33,10 @@ type MsgWait struct {
// API is a low-level interface to the Filecoin network
type API interface {
// Auth
AuthVerify(ctx context.Context, token string) ([]string, error)
AuthNew(ctx context.Context, perms []string) ([]byte, error)
// chain
ChainHead(context.Context) (*chain.TipSet, error) // TODO: check serialization

View File

@ -1,13 +1,15 @@
package client
import (
"net/http"
"github.com/filecoin-project/go-lotus/api"
"github.com/filecoin-project/go-lotus/lib/jsonrpc"
)
// NewRPC creates a new http jsonrpc client.
func NewRPC(addr string) (api.API, error) {
func NewRPC(addr string, requestHeader http.Header) (api.API, error) {
var res api.Struct
_, err := jsonrpc.NewClient(addr, "Filecoin", &res.Internal)
_, err := jsonrpc.NewClient(addr, "Filecoin", &res.Internal, requestHeader)
return &res, err
}

86
api/permissioned.go Normal file
View File

@ -0,0 +1,86 @@
package api
import (
"context"
"reflect"
"golang.org/x/xerrors"
)
type permKey int
var permCtxKey permKey
const (
// When changing these, update docs/API.md too
PermRead = "read" // default
PermWrite = "write"
PermSign = "sign" // Use wallet keys for signing
PermAdmin = "admin" // Manage permissions
)
var AllPermissions = []string{PermRead, PermWrite, PermSign, PermAdmin}
var defaultPerms = []string{PermRead}
func WithPerm(ctx context.Context, perms []string) context.Context {
return context.WithValue(ctx, permCtxKey, perms)
}
func Permissioned(a API) API {
var out Struct
rint := reflect.ValueOf(&out.Internal).Elem()
ra := reflect.ValueOf(a)
for f := 0; f < rint.NumField(); f++ {
field := rint.Type().Field(f)
requiredPerm := field.Tag.Get("perm")
if requiredPerm == "" {
panic("missing 'perm' tag on " + field.Name)
}
// Validate perm tag
ok := false
for _, perm := range AllPermissions {
if requiredPerm == perm {
ok = true
break
}
}
if !ok {
panic("unknown 'perm' tag on " + field.Name)
}
fn := ra.MethodByName(field.Name)
rint.Field(f).Set(reflect.MakeFunc(field.Type, func(args []reflect.Value) (results []reflect.Value) {
ctx := args[0].Interface().(context.Context)
callerPerms, ok := ctx.Value(permCtxKey).([]string)
if !ok {
callerPerms = defaultPerms
}
for _, callerPerm := range callerPerms {
if callerPerm == requiredPerm {
return fn.Call(args)
}
}
err := xerrors.Errorf("missing permission to invoke '%s' (need '%s')", field.Name, requiredPerm)
rerr := reflect.ValueOf(&err).Elem()
if field.Type.NumOut() == 2 {
return []reflect.Value{
reflect.Zero(field.Type.Out(0)),
rerr,
}
} else {
return []reflect.Value{rerr}
}
}))
}
return &out
}

View File

@ -11,41 +11,55 @@ import (
"github.com/libp2p/go-libp2p-core/peer"
)
// All permissions are listed in permissioned.go
var _ = AllPermissions
// Struct implements API passing calls to user-provided function values.
type Struct struct {
Internal struct {
ID func(context.Context) (peer.ID, error)
Version func(context.Context) (Version, error)
AuthVerify func(ctx context.Context, token string) ([]string, error) `perm:"read"`
AuthNew func(ctx context.Context, perms []string) ([]byte, error) `perm:"admin"`
ChainSubmitBlock func(ctx context.Context, blk *chain.BlockMsg) error
ChainHead func(context.Context) (*chain.TipSet, error)
ChainGetRandomness func(context.Context, *chain.TipSet) ([]byte, error)
ChainWaitMsg func(context.Context, cid.Cid) (*MsgWait, error)
ChainGetBlock func(context.Context, cid.Cid) (*chain.BlockHeader, error)
ChainGetBlockMessages func(context.Context, cid.Cid) ([]*chain.SignedMessage, error)
ID func(context.Context) (peer.ID, error) `perm:"read"`
Version func(context.Context) (Version, error) `perm:"read"`
MpoolPending func(context.Context, *chain.TipSet) ([]*chain.SignedMessage, error)
MpoolPush func(context.Context, *chain.SignedMessage) error
ChainSubmitBlock func(ctx context.Context, blk *chain.BlockMsg) error `perm:"write"`
ChainHead func(context.Context) (*chain.TipSet, error) `perm:"read"`
ChainGetRandomness func(context.Context, *chain.TipSet) ([]byte, error) `perm:"read"`
ChainWaitMsg func(context.Context, cid.Cid) (*MsgWait, error) `perm:"read"`
ChainGetBlock func(context.Context, cid.Cid) (*chain.BlockHeader, error) `perm:"read"`
ChainGetBlockMessages func(context.Context, cid.Cid) ([]*chain.SignedMessage, error) `perm:"read"`
MinerStart func(context.Context, address.Address) error
MinerCreateBlock func(context.Context, address.Address, *chain.TipSet, []chain.Ticket, chain.ElectionProof, []*chain.SignedMessage) (*chain.BlockMsg, error)
MpoolPending func(context.Context, *chain.TipSet) ([]*chain.SignedMessage, error) `perm:"read"`
MpoolPush func(context.Context, *chain.SignedMessage) error `perm:"write"`
WalletNew func(context.Context, string) (address.Address, error)
WalletList func(context.Context) ([]address.Address, error)
WalletBalance func(context.Context, address.Address) (types.BigInt, error)
WalletSign func(context.Context, address.Address, []byte) (*chain.Signature, error)
WalletDefaultAddress func(context.Context) (address.Address, error)
MpoolGetNonce func(context.Context, address.Address) (uint64, error)
MinerStart func(context.Context, address.Address) error `perm:"admin"`
MinerCreateBlock func(context.Context, address.Address, *chain.TipSet, []chain.Ticket, chain.ElectionProof, []*chain.SignedMessage) (*chain.BlockMsg, error) `perm:"write"`
ClientImport func(ctx context.Context, path string) (cid.Cid, error)
ClientListImports func(ctx context.Context) ([]Import, error)
WalletNew func(context.Context, string) (address.Address, error) `perm:"write"`
WalletList func(context.Context) ([]address.Address, error) `perm:"write"`
WalletBalance func(context.Context, address.Address) (types.BigInt, error) `perm:"read"`
WalletSign func(context.Context, address.Address, []byte) (*chain.Signature, error) `perm:"sign"`
WalletDefaultAddress func(context.Context) (address.Address, error) `perm:"write"`
MpoolGetNonce func(context.Context, address.Address) (uint64, error) `perm:"read"`
NetPeers func(context.Context) ([]peer.AddrInfo, error)
NetConnect func(context.Context, peer.AddrInfo) error
NetAddrsListen func(context.Context) (peer.AddrInfo, error)
ClientImport func(ctx context.Context, path string) (cid.Cid, error) `perm:"write"`
ClientListImports func(ctx context.Context) ([]Import, error) `perm:"read"`
NetPeers func(context.Context) ([]peer.AddrInfo, error) `perm:"read"`
NetConnect func(context.Context, peer.AddrInfo) error `perm:"write"`
NetAddrsListen func(context.Context) (peer.AddrInfo, error) `perm:"read"`
}
}
func (c *Struct) AuthVerify(ctx context.Context, token string) ([]string, error) {
return c.Internal.AuthVerify(ctx, token)
}
func (c *Struct) AuthNew(ctx context.Context, perms []string) ([]byte, error) {
return c.Internal.AuthNew(ctx, perms)
}
func (c *Struct) ClientListImports(ctx context.Context) ([]Import, error) {
return c.Internal.ClientListImports(ctx)
}

View File

@ -1,16 +1,16 @@
package types
// KeyInfo is used for storying keys in KeyStore
// KeyInfo is used for storing keys in KeyStore
type KeyInfo struct {
Type string
PrivateKey []byte
}
// KeyStore is used for storying secret keys
// KeyStore is used for storing secret keys
type KeyStore interface {
// List lists all the keys stored in the KeyStore
List() ([]string, error)
// Get gets a key out of keystore and returns KeyInfo coresponding to named key
// Get gets a key out of keystore and returns KeyInfo corresponding to named key
Get(string) (KeyInfo, error)
// Put saves a key info under given name
Put(string, KeyInfo) error

View File

@ -2,10 +2,12 @@ package cli
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
logging "github.com/ipfs/go-log"
manet "github.com/multiformats/go-multiaddr-net"
"gopkg.in/urfave/cli.v2"
@ -14,6 +16,8 @@ import (
"github.com/filecoin-project/go-lotus/node/repo"
)
var log = logging.Logger("cli")
const (
metadataContext = "context"
)
@ -35,7 +39,16 @@ func getAPI(ctx *cli.Context) (api.API, error) {
if err != nil {
return nil, err
}
return client.NewRPC("ws://" + addr + "/rpc/v0")
var headers http.Header
token, err := r.APIToken()
if err != nil {
log.Warnf("Couldn't load CLI token, capabilities may be limited: %w", err)
} else {
headers = http.Header{}
headers.Add("Authorization", "Bearer "+string(token))
}
return client.NewRPC("ws://"+addr+"/rpc/v0", headers)
}
// reqContext returns context for cli execution. Calling it for the first time

View File

@ -49,7 +49,12 @@ var Cmd = &cli.Command{
return err
}
// Write cli token to the repo if not there yet
if _, err := api.AuthNew(ctx, nil); err != nil {
return err
}
// TODO: properly parse api endpoint (or make it a URL)
return serveRPC(api, "127.0.0.1:"+cctx.String("api"))
return serveRPC(api, "127.0.0.1:"+cctx.String("api"), api.AuthVerify)
},
}

View File

@ -1,15 +1,23 @@
package daemon
import (
"context"
"github.com/filecoin-project/go-lotus/lib/auth"
"net/http"
"github.com/filecoin-project/go-lotus/api"
"github.com/filecoin-project/go-lotus/lib/jsonrpc"
)
func serveRPC(api api.API, addr string) error {
func serveRPC(a api.API, addr string, verify func(ctx context.Context, token string) ([]string, error)) error {
rpcServer := jsonrpc.NewServer()
rpcServer.Register("Filecoin", api)
http.Handle("/rpc/v0", rpcServer)
rpcServer.Register("Filecoin", api.Permissioned(a))
authHandler := &auth.Handler{
Verify: verify,
Next: rpcServer.ServeHTTP,
}
http.Handle("/rpc/v0", authHandler)
return http.ListenAndServe(addr, http.DefaultServeMux)
}

25
docs/API.md Normal file
View File

@ -0,0 +1,25 @@
TODO: make this into a nicer doc
### Endpoints
By default `127.0.0.1:1234` - daemon stores the api endpoint multiaddr in `~/.lotus/api`
* `http://[api:port]/rpc/v0` - jsonrpc http endpoint
* `ws://[api:port]/rpc/v0` - jsonrpc websocket endpoint
### Auth:
JWT in the `Authorization: Bearer <token>` http header
Permissions:
* `read` - Read node state, no private data
* `write` - Write to local store / chain, read private data
* `sign` - Use private keys stored in wallet for signing
* `admin` - Manage permissions
Payload:
```json
{
"Allow": ["read", "write", ...]
}
```

2
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/filecoin-project/go-bls-sigs v0.0.0-20190718224239-4bc4b8a7bbf8
github.com/filecoin-project/go-leb128 v0.0.0-20190212224330-8d79a5489543
github.com/gbrlsnchs/jwt/v3 v3.0.0-beta.1
github.com/gorilla/websocket v1.4.0
github.com/ipfs/go-bitswap v0.1.5
github.com/ipfs/go-block-format v0.0.2
@ -67,7 +68,6 @@ require (
go4.org v0.0.0-20190313082347-94abd6928b1d // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
google.golang.org/appengine v1.4.0 // indirect
gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect
)

2
go.sum
View File

@ -52,6 +52,8 @@ github.com/filecoin-project/go-leb128 v0.0.0-20190212224330-8d79a5489543 h1:aMJG
github.com/filecoin-project/go-leb128 v0.0.0-20190212224330-8d79a5489543/go.mod h1:mjrHv1cDGJWDlGmC0eDc1E5VJr8DmL9XMUcaFwiuKg8=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gbrlsnchs/jwt/v3 v3.0.0-beta.1 h1:EzDjxMg43q1tA2c0MV3tNbaontnHLplHyFF6M5KiVP0=
github.com/gbrlsnchs/jwt/v3 v3.0.0-beta.1/go.mod h1:0eHX/BVySxPc6SE2mZRoppGq7qcEagxdmQnA3dzork8=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=

42
lib/auth/handler.go Normal file
View File

@ -0,0 +1,42 @@
package auth
import (
"context"
"net/http"
"strings"
"github.com/filecoin-project/go-lotus/api"
logging "github.com/ipfs/go-log"
)
var log = logging.Logger("auth")
type Handler struct {
Verify func(ctx context.Context, token string) ([]string, error)
Next http.HandlerFunc
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token := r.Header.Get("Authorization")
if token != "" {
if !strings.HasPrefix(token, "Bearer ") {
log.Warn("missing Bearer prefix in auth header")
w.WriteHeader(401)
return
}
token = strings.TrimPrefix(token, "Bearer ")
allow, err := h.Verify(ctx, token)
if err != nil {
log.Warnf("JWT Verification failed: %s", err)
w.WriteHeader(401)
return
}
ctx = api.WithPerm(ctx, allow)
}
h.Next(w, r.WithContext(ctx))
}

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"sync/atomic"
@ -57,7 +58,7 @@ type ClientCloser func()
// handler must be pointer to a struct with function fields
// Returned value closes the client connection
// TODO: Example
func NewClient(addr string, namespace string, handler interface{}) (ClientCloser, error) {
func NewClient(addr string, namespace string, handler interface{}, requestHeader http.Header) (ClientCloser, error) {
htyp := reflect.TypeOf(handler)
if htyp.Kind() != reflect.Ptr {
return nil, xerrors.New("expected handler to be a pointer")
@ -71,7 +72,7 @@ func NewClient(addr string, namespace string, handler interface{}) (ClientCloser
var idCtr int64
conn, _, err := websocket.DefaultDialer.Dial(addr, nil)
conn, _, err := websocket.DefaultDialer.Dial(addr, requestHeader)
if err != nil {
return nil, err
}

View File

@ -73,7 +73,7 @@ func TestRPC(t *testing.T) {
AddGet func(int) int
StringMatch func(t TestType, i2 int64) (out TestOut, err error)
}
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &client)
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &client, nil)
require.NoError(t, err)
defer closer()
@ -111,7 +111,7 @@ func TestRPC(t *testing.T) {
var noret struct {
Add func(int)
}
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noret)
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noret, nil)
require.NoError(t, err)
// this one should actually work
@ -122,7 +122,7 @@ func TestRPC(t *testing.T) {
var noparam struct {
Add func()
}
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noparam)
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noparam, nil)
require.NoError(t, err)
// shouldn't panic
@ -132,7 +132,7 @@ func TestRPC(t *testing.T) {
var erronly struct {
AddGet func() (int, error)
}
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &erronly)
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &erronly, nil)
require.NoError(t, err)
_, err = erronly.AddGet()
@ -144,7 +144,7 @@ func TestRPC(t *testing.T) {
var wrongtype struct {
Add func(string) error
}
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &wrongtype)
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &wrongtype, nil)
require.NoError(t, err)
err = wrongtype.Add("not an int")
@ -156,7 +156,7 @@ func TestRPC(t *testing.T) {
var notfound struct {
NotThere func(string) error
}
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &notfound)
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &notfound, nil)
require.NoError(t, err)
err = notfound.NotThere("hello?")
@ -203,7 +203,7 @@ func TestCtx(t *testing.T) {
var client struct {
Test func(ctx context.Context)
}
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "CtxHandler", &client)
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "CtxHandler", &client, nil)
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
@ -224,7 +224,7 @@ func TestCtx(t *testing.T) {
var noCtxClient struct {
Test func()
}
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "CtxHandler", &noCtxClient)
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "CtxHandler", &noCtxClient, nil)
if err != nil {
t.Fatal(err)
}
@ -264,7 +264,7 @@ func TestUnmarshalableResult(t *testing.T) {
testServ := httptest.NewServer(rpcServer)
defer testServ.Close()
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "Handler", &client)
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "Handler", &client, nil)
require.NoError(t, err)
defer closer()
@ -325,7 +325,7 @@ func TestChan(t *testing.T) {
testServ := httptest.NewServer(rpcServer)
defer testServ.Close()
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client)
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client, nil)
require.NoError(t, err)
defer closer()

View File

@ -1,11 +1,11 @@
package jsonrpc
import (
"context"
"encoding/json"
"github.com/gorilla/websocket"
"io"
"net/http"
"github.com/gorilla/websocket"
)
const (
@ -28,7 +28,7 @@ func NewServer() *RPCServer {
var upgrader = websocket.Upgrader{}
func (s *RPCServer) handleWS(w http.ResponseWriter, r *http.Request) {
func (s *RPCServer) handleWS(ctx context.Context, w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error(err)
@ -39,7 +39,7 @@ func (s *RPCServer) handleWS(w http.ResponseWriter, r *http.Request) {
(&wsConn{
conn: c,
handler: s.methods,
}).handleWsConn(r.Context())
}).handleWsConn(ctx)
if err := c.Close(); err != nil {
log.Error(err)
@ -49,12 +49,14 @@ func (s *RPCServer) handleWS(w http.ResponseWriter, r *http.Request) {
// TODO: return errors to clients per spec
func (s *RPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if r.Header.Get("Connection") == "Upgrade" {
s.handleWS(w, r)
s.handleWS(ctx, w, r)
return
}
s.methods.handleReader(r.Context(), r.Body, w, rpcError)
s.methods.handleReader(ctx, r.Body, w, rpcError)
}
func rpcError(wf func(func(io.Writer)), req *request, code int, err error) {

View File

@ -10,22 +10,50 @@ import (
"github.com/filecoin-project/go-lotus/chain/types"
"github.com/filecoin-project/go-lotus/miner"
"github.com/filecoin-project/go-lotus/node/client"
"github.com/filecoin-project/go-lotus/node/modules"
"github.com/gbrlsnchs/jwt/v3"
"github.com/ipfs/go-cid"
logging "github.com/ipfs/go-log"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/peer"
pubsub "github.com/libp2p/go-libp2p-pubsub"
ma "github.com/multiformats/go-multiaddr"
"golang.org/x/xerrors"
)
var log = logging.Logger("node")
type API struct {
client.LocalStorage
Host host.Host
Chain *chain.ChainStore
PubSub *pubsub.PubSub
Mpool *chain.MessagePool
Wallet *chain.Wallet
Host host.Host
Chain *chain.ChainStore
PubSub *pubsub.PubSub
Mpool *chain.MessagePool
Wallet *chain.Wallet
APISecret *modules.APIAlg
}
type jwtPayload struct {
Allow []string
}
func (a *API) AuthVerify(ctx context.Context, token string) ([]string, error) {
var payload jwtPayload
if _, err := jwt.Verify([]byte(token), (*jwt.HMACSHA)(a.APISecret), &payload); err != nil {
return nil, xerrors.Errorf("JWT Verification failed: %w", err)
}
return payload.Allow, nil
}
func (a *API) AuthNew(ctx context.Context, perms []string) ([]byte, error) {
p := jwtPayload{
Allow: perms, // TODO: consider checking validity
}
return jwt.Sign(&p, (*jwt.HMACSHA)(a.APISecret))
}
func (a *API) ChainSubmitBlock(ctx context.Context, blk *chain.BlockMsg) error {

View File

@ -70,6 +70,7 @@ const (
HandleIncomingBlocksKey
HandleIncomingMessagesKey
// daemon
SetApiEndpointKey
_nInvokes // keep this last
@ -225,6 +226,8 @@ func Repo(r repo.Repo) Option {
Override(new(peer.ID), peer.IDFromPublicKey),
Override(new(types.KeyStore), modules.KeyStore),
Override(new(*modules.APIAlg), modules.APISecret),
)
}

View File

@ -2,6 +2,12 @@ package modules
import (
"context"
"crypto/rand"
"github.com/filecoin-project/go-lotus/api"
"github.com/gbrlsnchs/jwt/v3"
"golang.org/x/xerrors"
"io"
"io/ioutil"
"path/filepath"
"github.com/ipfs/go-bitswap"
@ -70,6 +76,51 @@ func KeyStore(lr repo.LockedRepo) (types.KeyStore, error) {
return lr.KeyStore()
}
const JWTSecretName = "auth-jwt-private"
type APIAlg jwt.HMACSHA
type jwtPayload struct {
Allow []string
}
func APISecret(keystore types.KeyStore, lr repo.LockedRepo) (*APIAlg, error) {
key, err := keystore.Get(JWTSecretName)
if err != nil {
log.Warn("Generating new API secret")
sk, err := ioutil.ReadAll(io.LimitReader(rand.Reader, 32))
if err != nil {
return nil, err
}
key = types.KeyInfo{
Type: "jwt-hmac-secret",
PrivateKey: sk,
}
if err := keystore.Put(JWTSecretName, key); err != nil {
return nil, xerrors.Errorf("writing API secret: %w", err)
}
// TODO: make this configurable
p := jwtPayload{
Allow: api.AllPermissions,
}
cliToken, err := jwt.Sign(&p, jwt.NewHS256(key.PrivateKey))
if err != nil {
return nil, err
}
if err := lr.SetAPIToken(cliToken); err != nil {
return nil, err
}
}
return (*APIAlg)(jwt.NewHS256(key.PrivateKey)), nil
}
func Datastore(r repo.LockedRepo) (datastore.Batching, error) {
return r.Datastore("/metadata")
}

View File

@ -57,7 +57,7 @@ func rpcBuilder(t *testing.T, n int) []api.API {
testServ := httptest.NewServer(rpcServer) // todo: close
var err error
out[i], err = client.NewRPC("ws://" + testServ.Listener.Addr().String())
out[i], err = client.NewRPC("ws://"+testServ.Listener.Addr().String(), nil)
if err != nil {
t.Fatal(err)
}

View File

@ -27,6 +27,7 @@ import (
const (
fsAPI = "api"
fsAPIToken = "token"
fsConfig = "config.toml"
fsDatastore = "datastore"
fsLibp2pKey = "libp2p.priv"
@ -109,6 +110,20 @@ func (fsr *FsRepo) APIEndpoint() (multiaddr.Multiaddr, error) {
return apima, nil
}
func (fsr *FsRepo) APIToken() ([]byte, error) {
p := filepath.Join(fsr.path, fsAPIToken)
f, err := os.Open(p)
if os.IsNotExist(err) {
return nil, ErrNoAPIEndpoint
} else if err != nil {
return nil, err
}
defer f.Close() //nolint: errcheck // Read only op
return ioutil.ReadAll(f)
}
// Lock acquires exclusive lock on this repo
func (fsr *FsRepo) Lock() (LockedRepo, error) {
locked, err := fslock.Locked(fsr.path, fsLock)
@ -245,6 +260,13 @@ func (fsr *fsLockedRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error {
return ioutil.WriteFile(fsr.join(fsAPI), []byte(ma.String()), 0644)
}
func (fsr *fsLockedRepo) SetAPIToken(token []byte) error {
if err := fsr.stillValid(); err != nil {
return err
}
return ioutil.WriteFile(fsr.join(fsAPIToken), token, 0600)
}
func (fsr *fsLockedRepo) KeyStore() (types.KeyStore, error) {
if err := fsr.stillValid(); err != nil {
return nil, err

View File

@ -13,6 +13,7 @@ import (
var (
ErrNoAPIEndpoint = errors.New("API not running (no endpoint)")
ErrNoAPIToken = errors.New("API token not set")
ErrRepoAlreadyLocked = errors.New("repo is already locked")
ErrClosedRepo = errors.New("repo is no longer open")
@ -24,6 +25,9 @@ type Repo interface {
// APIEndpoint returns multiaddress for communication with Lotus API
APIEndpoint() (multiaddr.Multiaddr, error)
// APIToken returns JWT API Token for use in operations that require auth
APIToken() ([]byte, error)
// Lock locks the repo for exclusive use.
Lock() (LockedRepo, error)
}
@ -45,6 +49,9 @@ type LockedRepo interface {
// so it can be read by API clients
SetAPIEndpoint(multiaddr.Multiaddr) error
// SetAPIToken sets JWT API Token for CLI
SetAPIToken([]byte) error
// KeyStore returns store of private keys for Filecoin transactions
KeyStore() (types.KeyStore, error)

View File

@ -18,7 +18,8 @@ import (
type MemRepo struct {
api struct {
sync.Mutex
ma multiaddr.Multiaddr
ma multiaddr.Multiaddr
token []byte
}
repoLock chan struct{}
@ -102,6 +103,15 @@ func (mem *MemRepo) APIEndpoint() (multiaddr.Multiaddr, error) {
return mem.api.ma, nil
}
func (mem *MemRepo) APIToken() ([]byte, error) {
mem.api.Lock()
defer mem.api.Unlock()
if mem.api.ma == nil {
return nil, ErrNoAPIToken
}
return mem.api.token, nil
}
func (mem *MemRepo) Lock() (LockedRepo, error) {
select {
case mem.repoLock <- struct{}{}:
@ -177,6 +187,16 @@ func (lmem *lockedMemRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error {
return nil
}
func (lmem *lockedMemRepo) SetAPIToken(token []byte) error {
if err := lmem.checkToken(); err != nil {
return err
}
lmem.mem.api.Lock()
lmem.mem.api.token = token
lmem.mem.api.Unlock()
return nil
}
func (lmem *lockedMemRepo) KeyStore() (types.KeyStore, error) {
if err := lmem.checkToken(); err != nil {
return nil, err