diff --git a/api/permissioned.go b/api/permissioned.go new file mode 100644 index 000000000..4a5ec3a99 --- /dev/null +++ b/api/permissioned.go @@ -0,0 +1,60 @@ +package api + +import ( + "context" + "fmt" + "reflect" +) + +type permKey int + +var permCtxKey permKey + +const ( + PermRead = "read" // default + PermWrite = "write" + // todo: more perms once needed (network / sign / call/invoke / miner / etc) +) + +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 == "" { + requiredPerm = PermRead + } + + 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) + } + } + + // TODO: return as error + panic(fmt.Sprintf("unauthorized call to %s", field.Name)) + })) + + } + + return &out +} diff --git a/api/struct.go b/api/struct.go index 6a47be1f0..f733fb220 100644 --- a/api/struct.go +++ b/api/struct.go @@ -17,7 +17,7 @@ type Struct struct { ID func(context.Context) (peer.ID, error) Version func(context.Context) (Version, error) - ChainSubmitBlock func(ctx context.Context, blk *chain.BlockMsg) error + ChainSubmitBlock func(ctx context.Context, blk *chain.BlockMsg) error `perm:"write"` ChainHead func(context.Context) (*chain.TipSet, error) ChainGetRandomness func(context.Context, *chain.TipSet) ([]byte, error) ChainWaitMsg func(context.Context, cid.Cid) (*MsgWait, error) @@ -27,21 +27,21 @@ type Struct struct { MpoolPending func(context.Context, *chain.TipSet) ([]*chain.SignedMessage, error) MpoolPush func(context.Context, *chain.SignedMessage) error - MinerStart func(context.Context, address.Address) error - MinerCreateBlock func(context.Context, address.Address, *chain.TipSet, []chain.Ticket, chain.ElectionProof, []*chain.SignedMessage) (*chain.BlockMsg, error) + MinerStart func(context.Context, address.Address) error `perm:"write"` + MinerCreateBlock func(context.Context, address.Address, *chain.TipSet, []chain.Ticket, chain.ElectionProof, []*chain.SignedMessage) (*chain.BlockMsg, error) `perm:"write"` - WalletNew func(context.Context, string) (address.Address, error) + WalletNew func(context.Context, string) (address.Address, error) `perm:"write"` 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) + WalletSign func(context.Context, address.Address, []byte) (*chain.Signature, error) `perm:"write"` WalletDefaultAddress func(context.Context) (address.Address, error) MpoolGetNonce func(context.Context, address.Address) (uint64, error) - ClientImport func(ctx context.Context, path string) (cid.Cid, error) + ClientImport func(ctx context.Context, path string) (cid.Cid, error) `perm:"write"` ClientListImports func(ctx context.Context) ([]Import, error) NetPeers func(context.Context) ([]peer.AddrInfo, error) - NetConnect func(context.Context, peer.AddrInfo) error + NetConnect func(context.Context, peer.AddrInfo) error `perm:"write"` NetAddrsListen func(context.Context) (peer.AddrInfo, error) } } diff --git a/daemon/rpc.go b/daemon/rpc.go index 48906cf55..fde85bc98 100644 --- a/daemon/rpc.go +++ b/daemon/rpc.go @@ -7,9 +7,9 @@ import ( "github.com/filecoin-project/go-lotus/lib/jsonrpc" ) -func serveRPC(api api.API, addr string) error { +func serveRPC(a api.API, addr string) error { rpcServer := jsonrpc.NewServer() - rpcServer.Register("Filecoin", api) + rpcServer.Register("Filecoin", api.Permissioned(a)) http.Handle("/rpc/v0", rpcServer) return http.ListenAndServe(addr, http.DefaultServeMux) } diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 000000000..6e53ddcda --- /dev/null +++ b/docs/API.md @@ -0,0 +1,23 @@ +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 ` http header + +Permissions: +* `read` - read node state, no private data +* `write` - basically root access, for now + +Payload: +```json +{ + "Allow": ["read", "write", ...] +} +``` diff --git a/go.mod b/go.mod index bbdb89d66..9b6cb000d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a6aa7f2a8..f099500de 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/lib/jsonrpc/auth.go b/lib/jsonrpc/auth.go new file mode 100644 index 000000000..916f97afd --- /dev/null +++ b/lib/jsonrpc/auth.go @@ -0,0 +1,13 @@ +package jsonrpc + +import ( + "github.com/gbrlsnchs/jwt/v3" +) + +var secret = jwt.NewHS256([]byte("todo: get me from the repo")) + +type jwtPayload struct { + Allow []string +} + + diff --git a/lib/jsonrpc/server.go b/lib/jsonrpc/server.go index d6111e0d7..189862779 100644 --- a/lib/jsonrpc/server.go +++ b/lib/jsonrpc/server.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io" "net/http" + "strings" "github.com/gorilla/websocket" ) @@ -49,6 +50,17 @@ 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) { + token := r.Header.Get("Authorization") + if token != "" { + if !strings.HasPrefix(token, "Bearer ") { + w.WriteHeader(401) + return + } + token = token[len("Bearer "):] + + + } + if r.Header.Get("Connection") == "Upgrade" { s.handleWS(w, r) return