Skip to content

Commit

Permalink
core: fix thread_interceptor owner check
Browse files Browse the repository at this point in the history
Signed-off-by: Sander Pick <[email protected]>
  • Loading branch information
sanderpick committed Feb 25, 2022
1 parent f134195 commit 686920c
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 36 deletions.
6 changes: 0 additions & 6 deletions .github/weekly-digest.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
MONGO_URI: mongodb://127.0.0.1:27018
IPFS_API_ADDR: /ip4/127.0.0.1/tcp/5012
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}
run: go test -timeout 60m -race ./api/... ./mongodb/...
run: go test -timeout 60m -race ./core/... ./api/... ./mongodb/...
buckets:
name: Buckets
runs-on: [self-hosted, hub2]
Expand Down
2 changes: 1 addition & 1 deletion api/hubd/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (c *Client) Close() error {
return c.conn.Close()
}

// Info provides API build information.
// BuildInfo provides API build information.
func (c *Client) BuildInfo(ctx context.Context) (*pb.BuildInfoResponse, error) {
return c.c.BuildInfo(ctx, &pb.BuildInfoRequest{})
}
Expand Down
3 changes: 2 additions & 1 deletion core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ var (

// blockMethods are always blocked by auth.
blockMethods = []string{
"/threads.pb.API/ListDBs",
"/threads.pb.API/ListDBs", // we don't want anyone to list all dbs
"/threads.net.pb.API/Subscribe", // there is no way to limit this API to thread owners
}

powergateServiceName = "powergate.user.v1.UserService"
Expand Down
115 changes: 88 additions & 27 deletions core/thread_interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,33 +84,17 @@ func (t *Textile) threadInterceptor() grpc.UnaryServerInterceptor {
default:
// If we're dealing with an existing thread, make sure that the owner
// owns the thread directly or via an API key.
threadID, ok := common.ThreadIDFromContext(ctx)
if ok {
th, err := t.collections.Threads.Get(ctx, threadID, account.Owner().Key)
if err != nil && errors.Is(err, mongo.ErrNoDocuments) {
// Allow non-owners to interact with a limited set of APIs.
var isAllowed bool
for _, m := range allowedCrossUserMethods {
if method == m {
isAllowed = true
break
}
}
if !isAllowed {
return nil, status.Error(codes.PermissionDenied, "User does not own thread")
}
} else if err != nil {
return nil, err
}
if th != nil {
key, _ := mdb.APIKeyFromContext(ctx)
if key != nil && key.Type == mdb.UserKey {
// Extra user check for user API keys.
if key.Key != th.Key {
return nil, status.Error(codes.PermissionDenied, "Bad API key")
}
}
}
// Hub APIs that deal with threads require the id to be passed in the context.
// go-threads APIs take the ID as a request param, which means we have to
// extract it from the request.
threadIDFromCtx, _ := common.ThreadIDFromContext(ctx)
threadIDFromReq, err := getThreadIDFromRequest(method, req)
if err != nil {
return nil, err
}
ids := []thread.ID{threadIDFromCtx, threadIDFromReq}
if err := t.checkThreadOwner(ctx, method, ids, account); err != nil {
return nil, err
}
}

Expand Down Expand Up @@ -173,3 +157,80 @@ func (t *Textile) threadInterceptor() grpc.UnaryServerInterceptor {
return res, nil
}
}

func (t *Textile) checkThreadOwner(
ctx context.Context,
method string,
threadIDs []thread.ID,
account *mdb.AccountCtx,
) error {
for _, id := range threadIDs {
if !id.Defined() {
continue
}
th, err := t.collections.Threads.Get(ctx, id, account.Owner().Key)
if err != nil && errors.Is(err, mongo.ErrNoDocuments) {
// Allow non-owners to interact with a limited set of APIs.
var isAllowed bool
for _, m := range allowedCrossUserMethods {
if method == m {
isAllowed = true
break
}
}
if !isAllowed {
return status.Error(codes.PermissionDenied, "User does not own thread")
}
} else if err != nil {
return err
}
if th != nil {
key, _ := mdb.APIKeyFromContext(ctx)
if key != nil && key.Type == mdb.UserKey {
// Extra user check for user API keys.
if key.Key != th.Key {
return status.Error(codes.PermissionDenied, "Bad API key")
}
}
}
}
return nil
}

func getThreadIDFromRequest(method string, req interface{}) (thread.ID, error) {
var id thread.ID
var err error
switch method {
case "/threads.pb.API/GetDBInfo":
id, err = thread.Cast(req.(*dbpb.GetDBInfoRequest).DbID)
case "/threads.pb.API/DeleteDB":
id, err = thread.Cast(req.(*dbpb.DeleteDBRequest).DbID)
case "/threads.pb.API/NewCollection":
id, err = thread.Cast(req.(*dbpb.NewCollectionRequest).DbID)
case "/threads.pb.API/UpdateCollection":
id, err = thread.Cast(req.(*dbpb.UpdateCollectionRequest).DbID)
case "/threads.pb.API/DeleteCollection":
id, err = thread.Cast(req.(*dbpb.DeleteCollectionRequest).DbID)
case "/threads.pb.API/GetCollectionInfo":
id, err = thread.Cast(req.(*dbpb.GetCollectionInfoRequest).DbID)
case "/threads.pb.API/GetCollectionIndexes":
id, err = thread.Cast(req.(*dbpb.GetCollectionIndexesRequest).DbID)
case "/threads.pb.API/ListCollections":
id, err = thread.Cast(req.(*dbpb.ListCollectionsRequest).DbID)
case "/threads.net.pb.API/GetThread":
id, err = thread.Cast(req.(*netpb.GetThreadRequest).ThreadID)
case "/threads.net.pb.API/PullThread":
id, err = thread.Cast(req.(*netpb.PullThreadRequest).ThreadID)
case "/threads.net.pb.API/DeleteThread":
id, err = thread.Cast(req.(*netpb.DeleteThreadRequest).ThreadID)
case "/threads.net.pb.API/AddReplicator":
id, err = thread.Cast(req.(*netpb.AddReplicatorRequest).ThreadID)
case "/threads.net.pb.API/CreateRecord":
id, err = thread.Cast(req.(*netpb.CreateRecordRequest).ThreadID)
case "/threads.net.pb.API/AddRecord":
id, err = thread.Cast(req.(*netpb.AddRecordRequest).ThreadID)
case "/threads.net.pb.API/GetRecord":
id, err = thread.Cast(req.(*netpb.GetRecordRequest).ThreadID)
}
return id, err
}
206 changes: 206 additions & 0 deletions core/thread_interceptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package core_test

import (
"context"
"crypto/rand"
"os"
"testing"

"github.com/ipfs/go-cid"
cbornode "github.com/ipfs/go-ipld-cbor"
"github.com/libp2p/go-libp2p-core/crypto"
"github.com/libp2p/go-libp2p-core/peer"
ma "github.com/multiformats/go-multiaddr"
mh "github.com/multiformats/go-multihash"
"github.com/stretchr/testify/require"
"github.com/textileio/crypto/symmetric"
tc "github.com/textileio/go-threads/api/client"
"github.com/textileio/go-threads/cbor"
coredb "github.com/textileio/go-threads/core/db"
"github.com/textileio/go-threads/core/thread"
"github.com/textileio/go-threads/db"
nc "github.com/textileio/go-threads/net/api/client"
tutil "github.com/textileio/go-threads/util"
"github.com/textileio/textile/v2/api/apitest"
"github.com/textileio/textile/v2/api/common"
hc "github.com/textileio/textile/v2/api/hubd/client"
"github.com/textileio/textile/v2/core"
"google.golang.org/grpc"
)

func TestMain(m *testing.M) {
cleanup := func() {}
if os.Getenv("SKIP_SERVICES") != "true" {
cleanup = apitest.StartServices()
}
exitVal := m.Run()
cleanup()
os.Exit(exitVal)
}

func Test_ThreadsDB(t *testing.T) {
t.Parallel()
conf, hub, threads, _ := setup(t, nil)
ctx := context.Background()

dev1 := apitest.Signup(t, hub, conf, apitest.NewUsername(), apitest.NewEmail())
dev2 := apitest.Signup(t, hub, conf, apitest.NewUsername(), apitest.NewEmail())
ctx1 := common.NewSessionContext(ctx, dev1.Session)
ctx2 := common.NewSessionContext(ctx, dev2.Session)

// dev1 creates a thread
id := thread.NewIDV1(thread.Raw, 32)
err := threads.NewDB(ctx1, id, db.WithNewManagedCollections(collectionCongig))
require.NoError(t, err)

// dev1 creates an instance
_, err = threads.Create(ctx1, id, "Dogs", tc.Instances{&Dog{Name: "Fido", Comments: []Comment{}}})
require.NoError(t, err)

// dev2 create an instance (should be fine, this can be controlled with write validator)
_, err = threads.Create(ctx2, id, "Dogs", tc.Instances{&Dog{Name: "Fido", Comments: []Comment{}}})
require.NoError(t, err)

// dev2 attempts all the blocked methods
_, err = threads.GetDBInfo(ctx2, id)
require.Error(t, err)
err = threads.DeleteDB(ctx2, id)
require.Error(t, err)
_, err = threads.ListDBs(ctx2)
require.Error(t, err)
err = threads.NewCollection(ctx2, id, collectionCongig)
require.Error(t, err)
err = threads.UpdateCollection(ctx2, id, collectionCongig)
require.Error(t, err)
err = threads.DeleteCollection(ctx2, id, "Dogs")
require.Error(t, err)
_, err = threads.GetCollectionInfo(ctx2, id, "Dogs")
require.Error(t, err)
_, err = threads.GetCollectionIndexes(ctx2, id, "Dogs")
require.Error(t, err)
_, err = threads.ListCollections(ctx2, id)
require.Error(t, err)
}

func TestClient_ThreadsNet(t *testing.T) {
t.Parallel()
conf, hub, _, net := setup(t, nil)
ctx := context.Background()

dev1 := apitest.Signup(t, hub, conf, apitest.NewUsername(), apitest.NewEmail())
dev2 := apitest.Signup(t, hub, conf, apitest.NewUsername(), apitest.NewEmail())
ctx1 := common.NewSessionContext(ctx, dev1.Session)
ctx2 := common.NewSessionContext(ctx, dev2.Session)

// dev1 creates a thread
id := thread.NewIDV1(thread.Raw, 32)
_, err := net.CreateThread(ctx1, id)
require.NoError(t, err)

// dev1 creates a record
body, err := cbornode.WrapObject(map[string]interface{}{
"foo": "bar",
"baz": []byte("howdy"),
}, mh.SHA2_256, -1)
require.NoError(t, err)
_, err = net.CreateRecord(ctx1, id, body)
require.NoError(t, err)

// dev2 attempts all the blocked methods
_, err = net.GetThread(ctx2, id)
require.Error(t, err)
err = net.PullThread(ctx2, id)
require.Error(t, err)
err = net.DeleteThread(ctx2, id)
require.Error(t, err)

sk, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)
pid, err := peer.IDFromPrivateKey(sk)
require.NoError(t, err)
addr, err := ma.NewMultiaddr("/p2p/" + pid.String())
require.NoError(t, err)
_, err = net.AddReplicator(ctx2, id, addr)
require.Error(t, err)

_, err = net.CreateRecord(ctx2, id, body)
require.Error(t, err)

event, err := cbor.CreateEvent(context.Background(), nil, body, symmetric.New())
if err != nil {
t.Fatal(err)
}
rec, err := cbor.CreateRecord(context.Background(), nil, cbor.CreateRecordConfig{
Block: event,
Prev: cid.Undef,
Key: sk,
PubKey: thread.NewLibp2pIdentity(sk).GetPublic(),
ServiceKey: thread.NewRandomServiceKey().Service(),
})
require.NoError(t, err)
err = net.AddRecord(ctx2, id, pid, rec)
require.Error(t, err)

_, err = net.GetRecord(ctx2, id, cid.Cid{})
require.Error(t, err)
}

func setup(t *testing.T, conf *core.Config) (core.Config, *hc.Client, *tc.Client, *nc.Client) {
if conf == nil {
tmp := apitest.DefaultTextileConfig(t)
conf = &tmp
}
return setupWithConf(t, *conf)
}

func setupWithConf(t *testing.T, conf core.Config) (core.Config, *hc.Client, *tc.Client, *nc.Client) {
apitest.MakeTextileWithConfig(t, conf)
target, err := tutil.TCPAddrFromMultiAddr(conf.AddrAPI)
require.NoError(t, err)
opts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithPerRPCCredentials(common.Credentials{})}
hubclient, err := hc.NewClient(target, opts...)
require.NoError(t, err)
threadsclient, err := tc.NewClient(target, opts...)
require.NoError(t, err)
threadsnetclient, err := nc.NewClient(target, opts...)
require.NoError(t, err)

t.Cleanup(func() {
require.NoError(t, err)
err = threadsclient.Close()
require.NoError(t, err)
})
return conf, hubclient, threadsclient, threadsnetclient
}

type Comment struct {
Body string
}

type Dog struct {
ID coredb.InstanceID `json:"_id"`
Name string
Comments []Comment
}

var collectionCongig = db.CollectionConfig{
Name: "Dogs",
Schema: tutil.SchemaFromInstance(&Dog{}, false),
WriteValidator: `
var type = event.patch.type
var patch = event.patch.json_patch
switch (type) {
case "delete":
return false
default:
if (patch.Name !== "Fido" && patch.Name != "Clyde") {
return false
}
return true
}
`,
ReadFilter: `
instance.Name = "Clyde"
return instance
`,
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/stripe/stripe-go/v72 v72.10.0
github.com/tchap/go-patricia v2.3.0+incompatible // indirect
github.com/textileio/crypto v0.0.0-20210929130053-08edebc3361a // indirect
github.com/textileio/dcrypto v0.0.1
github.com/textileio/go-assets v0.0.0-20200430191519-b341e634e2b7
github.com/textileio/go-ds-mongo v0.1.5
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1871,6 +1871,8 @@ github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPg
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/textileio/crypto v0.0.0-20210928200545-9b5a55171e1b h1:719JpryXudi0waRfW7evibrFSN8AMP/1MC683U1ICNU=
github.com/textileio/crypto v0.0.0-20210928200545-9b5a55171e1b/go.mod h1:p2BwrUs69ZtGA27KXYYOiToLs1PyPV5etdUFMko3d9c=
github.com/textileio/crypto v0.0.0-20210929130053-08edebc3361a h1:ECSeaC+8iE/EmAqsX3JltDebB5q2IsDWy4KQqkl5c+8=
github.com/textileio/crypto v0.0.0-20210929130053-08edebc3361a/go.mod h1:p2BwrUs69ZtGA27KXYYOiToLs1PyPV5etdUFMko3d9c=
github.com/textileio/dcrypto v0.0.1 h1:ftXQKd+CAM7a0XFrw+hJqizo+ux8+g5RttKjImZRc7U=
github.com/textileio/dcrypto v0.0.1/go.mod h1:rDlYXuL+HQwkyrxOR230zEUouRnlTwH6O5XWoPbmfcE=
github.com/textileio/dsutils v1.0.1/go.mod h1:A2RXLoDWyLTCuEmC41+WjC8jRsE7BeRy1c6s3acc3PY=
Expand All @@ -1891,6 +1893,8 @@ github.com/textileio/go-log/v2 v2.1.3-gke-1 h1:7e3xSUXQB8hn4uUe5fp41kLThW1o9T65g
github.com/textileio/go-log/v2 v2.1.3-gke-1/go.mod h1:DwACkjFS3kjZZR/4Spx3aPfSsciyslwUe5bxV8CEU2w=
github.com/textileio/go-threads v1.1.2-0.20211029155120-3479a196d9b7 h1:G984DCftarAL5DjWawFPjS0vi3fhtxRMKNOLEmtUzMM=
github.com/textileio/go-threads v1.1.2-0.20211029155120-3479a196d9b7/go.mod h1:pC5hdRsNeprQaXVJ9b/EKF/ZCiiYl2KQ6HEAlT/CVDo=
github.com/textileio/go-threads v1.1.2 h1:nZ0c310RdnKVVwIh29mky5UcwaOjltNZ3Ux5iNrQut0=
github.com/textileio/go-threads v1.1.2/go.mod h1:pC5hdRsNeprQaXVJ9b/EKF/ZCiiYl2KQ6HEAlT/CVDo=
github.com/textileio/powergate/v2 v2.3.0 h1:kelYh+ZWDQao1rL5YiMznQscd6CsDjgt6P/D1S5UYwQ=
github.com/textileio/powergate/v2 v2.3.0/go.mod h1:2j2NL1oevaVdrI6MpKfHnfgUOy1D4L7eP3I+1czxDjw=
github.com/textileio/swagger-ui v0.3.29-0.20210224180244-7d73a7a32fe7 h1:qUEurT6kJF+nFkiNjUPMJJ7hgg9OIDnb8iLn6VtBukE=
Expand Down

0 comments on commit 686920c

Please sign in to comment.