Skip to content

Commit

Permalink
Feat(backend+frontend): Trusted contacts
Browse files Browse the repository at this point in the history
Getting trusted entities only when requested. Ability to trust/untrust peers

---------

Co-authored-by: Eric Vicenti <[email protected]>
Co-authored-by: Alexandr Burdiyan <[email protected]>
  • Loading branch information
3 people authored Aug 7, 2023
1 parent 9aebc0a commit aca3996
Show file tree
Hide file tree
Showing 47 changed files with 1,591 additions and 374 deletions.
47 changes: 45 additions & 2 deletions backend/daemon/api/accounts/v1alpha/accounts.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Package accounts implements account functions.
package accounts

import (
"bytes"
"context"
"fmt"

"mintter/backend/core"
accounts "mintter/backend/genproto/accounts/v1alpha"
"mintter/backend/hyper"
Expand Down Expand Up @@ -81,7 +81,13 @@ func (srv *Server) GetAccount(ctx context.Context, in *accounts.GetAccountReques
DeviceId: pids,
}
}

istrusted, err := hypersql.IsTrustedAccount(conn, aid)
if err != nil {
return err
}
if istrusted.TrustedAccountsID != 0 {
acc.IsTrusted = true
}
return nil
}); err != nil {
return nil, err
Expand Down Expand Up @@ -172,6 +178,7 @@ func (srv *Server) UpdateProfile(ctx context.Context, in *accounts.Profile) (*ac
return srv.GetAccount(ctx, &accounts.GetAccountRequest{})
}

// UpdateProfile is public so it can be called from sites.
func UpdateProfile(ctx context.Context, me core.Identity, blobs *hyper.Storage, in *accounts.Profile) error {
eid := hyper.EntityID("hd://a/" + me.Account().Principal().String())

Expand Down Expand Up @@ -229,6 +236,42 @@ func UpdateProfile(ctx context.Context, me core.Identity, blobs *hyper.Storage,
return nil
}

// SetAccountTrust implements the corresponding gRPC method.
func (srv *Server) SetAccountTrust(ctx context.Context, in *accounts.SetAccountTrustRequest) (*accounts.Account, error) {
acc, err := core.DecodePrincipal(in.Id)
if err != nil {
return nil, err
}
if in.IsTrusted {
err = srv.blobs.SetAccountTrust(ctx, acc)
} else {
me, ok := srv.me.Get()
if !ok {
return nil, fmt.Errorf("account not initialized yet")
}
if acc.String() == me.Account().Principal().String() {
return nil, fmt.Errorf("cannot untrust self")
}
err = srv.blobs.UnsetAccountTrust(ctx, acc)
}

if err != nil {
return nil, err
}

updatedAcc, err := srv.GetAccount(ctx, &accounts.GetAccountRequest{
Id: acc.String(),
})
if err != nil {
return nil, err
}
if updatedAcc.IsTrusted != in.IsTrusted {
return nil, fmt.Errorf("Expected trusted %t but got %t", in.IsTrusted, updatedAcc.IsTrusted)
}

return updatedAcc, nil
}

// ListAccounts implements the corresponding gRPC method.
func (srv *Server) ListAccounts(ctx context.Context, in *accounts.ListAccountsRequest) (*accounts.ListAccountsResponse, error) {
me, err := srv.me.Await(ctx)
Expand Down
20 changes: 18 additions & 2 deletions backend/daemon/api/accounts/v1alpha/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ func TestGetAccount_Own(t *testing.T) {
DeviceId: "12D3KooWFMTJanyH3XttUC2AmS9fZnbeYsxbAjSEvyCeHVbHBX3C",
},
},
IsTrusted: true,
}

acc, err := alice.GetAccount(ctx, &accounts.GetAccountRequest{})
require.NoError(t, err)
testutil.ProtoEqual(t, want, acc, "accounts don't match")
Expand Down Expand Up @@ -67,12 +67,12 @@ func TestAPIUpdateProfile(t *testing.T) {
DeviceId: "12D3KooWFMTJanyH3XttUC2AmS9fZnbeYsxbAjSEvyCeHVbHBX3C",
},
},
IsTrusted: true,
}

updated, err := alice.UpdateProfile(ctx, want.Profile)
require.NoError(t, err)
testutil.ProtoEqual(t, want, updated, "account must be equal")

stored, err := alice.GetAccount(ctx, &accounts.GetAccountRequest{})
require.NoError(t, err)
testutil.ProtoEqual(t, want, stored, "get account must return updated account")
Expand All @@ -90,6 +90,7 @@ func TestAPIUpdateProfile(t *testing.T) {
DeviceId: "12D3KooWFMTJanyH3XttUC2AmS9fZnbeYsxbAjSEvyCeHVbHBX3C",
},
},
IsTrusted: true,
}

updated, err := alice.UpdateProfile(ctx, want.Profile)
Expand All @@ -102,6 +103,21 @@ func TestAPIUpdateProfile(t *testing.T) {
}
}

func TestTrustOwnAccount(t *testing.T) {
alice := newTestServer(t, "alice")
bob := coretest.NewTester("bob")
ctx := context.Background()
acc, err := alice.SetAccountTrust(ctx, &accounts.SetAccountTrustRequest{
Id: bob.Account.Principal().String(),
IsTrusted: true,
})
require.Error(t, err, "Alice must not have Bob's account")
require.Nil(t, acc)
acc, err = alice.GetAccount(ctx, &accounts.GetAccountRequest{}) //No id=own account
require.NoError(t, err)
require.True(t, acc.IsTrusted)
}

// TODO: update profile idempotent no change

func newTestServer(t *testing.T, name string) *Server {
Expand Down
5 changes: 5 additions & 0 deletions backend/daemon/api/daemon/v1alpha/daemon.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package daemon assembles everything to boot the mintterd program. It's like main, but made a separate package
// to be importable and testable by other packages, because package main can't be imported.
package daemon

import (
Expand Down Expand Up @@ -119,6 +121,9 @@ func Register(ctx context.Context, bs *hyper.Storage, account core.KeyPair, devi
return cid.Undef, err
}

if err = bs.SetAccountTrust(ctx, account.Principal()); err != nil {
return blob.CID, fmt.Errorf("Could not set own account to trusted: " + err.Error())
}
return blob.CID, nil
}

Expand Down
17 changes: 11 additions & 6 deletions backend/daemon/api/documents/v1alpha/documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ func (api *Server) GetPublication(ctx context.Context, in *documents.GetPublicat
eid := hyper.EntityID("hd://d/" + in.DocumentId)
version := hyper.Version(in.Version)

pub, err := api.loadPublication(ctx, eid, version)
pub, err := api.loadPublication(ctx, eid, version, in.TrustedOnly)
if err == nil {
return pub, nil
}
Expand All @@ -362,10 +362,10 @@ func (api *Server) GetPublication(ctx context.Context, in *documents.GetPublicat
return nil, status.Errorf(codes.NotFound, "failed to discover object %q at version %q", eid, version)
}

return api.loadPublication(ctx, eid, version)
return api.loadPublication(ctx, eid, version, in.TrustedOnly)
}

func (api *Server) loadPublication(ctx context.Context, docid hyper.EntityID, version hyper.Version) (docpb *documents.Publication, err error) {
func (api *Server) loadPublication(ctx context.Context, docid hyper.EntityID, version hyper.Version, trustedOnly bool) (docpb *documents.Publication, err error) {
var entity *hyper.Entity
if version != "" {
heads, err := hyper.Version(version).Parse()
Expand All @@ -378,7 +378,11 @@ func (api *Server) loadPublication(ctx context.Context, docid hyper.EntityID, ve
return nil, err
}
} else {
entity, err = api.blobs.LoadEntity(ctx, docid)
if trustedOnly {
entity, err = api.blobs.LoadTrustedEntity(ctx, docid)
} else {
entity, err = api.blobs.LoadEntity(ctx, docid)
}
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -443,8 +447,9 @@ func (api *Server) ListPublications(ctx context.Context, in *documents.ListPubli
for _, e := range entities {
docid := e.TrimPrefix("hd://d/")
pub, err := api.GetPublication(ctx, &documents.GetPublicationRequest{
DocumentId: docid,
LocalOnly: true,
DocumentId: docid,
LocalOnly: true,
TrustedOnly: in.TrustedOnly,
})
if err != nil {
continue
Expand Down
151 changes: 149 additions & 2 deletions backend/daemon/daemon_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,84 @@ func TestSite(t *testing.T) {
require.Error(t, err)
}

func TestTrustedChanges(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())

t.Cleanup(func() {
cancel()
})

trusted := makeTestApp(t, "alice", makeTestConfig(t), true)
untrusted := makeTestApp(t, "bob", makeTestConfig(t), true)

// Both peers connect connect to each other, to exchange documents. Does not mean they trust each other.
_, err := trusted.RPC.Networking.Connect(ctx, &networking.ConnectRequest{Addrs: getAddrs(t, untrusted)})
require.NoError(t, err)

// Create a document.
sharedDocument := publishDocument(t, ctx, trusted)

// Sync the document with the untusted peer so it can modify it.
_, err = untrusted.RPC.Daemon.ForceSync(ctx, &daemon.ForceSyncRequest{})
require.NoError(t, err)
var publicationList *documents.ListPublicationsResponse
require.Eventually(t, func() bool {
// List all documents which by default includes untrusted changes.
publicationList, err = untrusted.RPC.Documents.ListPublications(ctx, &documents.ListPublicationsRequest{})
require.NoError(t, err)
return len(publicationList.Publications) == 1
}, 1*time.Second, 100*time.Millisecond, "peer should have synced the document")

// Check that the received version is the one the initial author created.
require.Equal(t, sharedDocument.Version, publicationList.Publications[0].Version)
require.Equal(t, sharedDocument.Document.Author, publicationList.Publications[0].Document.Author)
require.Equal(t, sharedDocument.Document.Id, publicationList.Publications[0].Document.Id)

// Add an untrusted change
const anotherTitle = "New Document title leading to a new version"
newVersion := updateDocumenTitle(t, ctx, untrusted, sharedDocument.Document.Id, anotherTitle)
require.Equal(t, sharedDocument.Document.Id, newVersion.Document.Id)

// Send the document back to the creator which the new untrusted changes.
_, err = trusted.RPC.Daemon.ForceSync(ctx, &daemon.ForceSyncRequest{})
require.NoError(t, err)

require.Eventually(t, func() bool {
// List all documents which by default includes untrusted changes.
publicationList, err = trusted.RPC.Documents.ListPublications(ctx, &documents.ListPublicationsRequest{})
require.NoError(t, err)
return len(publicationList.Publications) == 1
}, 1*time.Second, 100*time.Millisecond, "peer should have synced the document")

// Check that the version is the latest one (untrusted).
require.Equal(t, newVersion.Version, publicationList.Publications[0].Version)
require.Equal(t, newVersion.Document.Author, publicationList.Publications[0].Document.Author)
require.Equal(t, newVersion.Document.Id, publicationList.Publications[0].Document.Id)

// Now ask for trusted changes only.
publicationList, err = trusted.RPC.Documents.ListPublications(ctx, &documents.ListPublicationsRequest{
TrustedOnly: true,
})
require.NoError(t, err)
require.Len(t, publicationList.Publications, 1, "same document, different version")

// Check that the version is the initial one, without any untrusted changes.
require.Equal(t, sharedDocument.Version, publicationList.Publications[0].Version)
require.Equal(t, sharedDocument.Document.Author, publicationList.Publications[0].Document.Author)
require.Equal(t, sharedDocument.Document.Id, publicationList.Publications[0].Document.Id)

// But the untrusted peer (last editor) should get the latest version since it was him
// the one who wrote the latest changes and he trusts himself by default.
publicationList, err = untrusted.RPC.Documents.ListPublications(ctx, &documents.ListPublicationsRequest{
TrustedOnly: true,
})
require.NoError(t, err)
require.Len(t, publicationList.Publications, 1, "same document, different version")
require.Equal(t, newVersion.Version, publicationList.Publications[0].Version)
require.Equal(t, newVersion.Document.Author, publicationList.Publications[0].Document.Author)
require.Equal(t, newVersion.Document.Id, publicationList.Publications[0].Document.Id)
}

func TestGateway(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -665,6 +743,7 @@ func TestPeriodicSync(t *testing.T) {
bacc := must.Do2(b.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{}))

require.Len(t, accs.Accounts, 1, msg)
bacc.IsTrusted = accs.Accounts[0].IsTrusted // just bc they synced they dont trust each other
testutil.ProtoEqual(t, bacc, accs.Accounts[0], "a must fetch b's account fully")
}

Expand All @@ -683,9 +762,9 @@ func TestMultiDevice(t *testing.T) {
Addrs: getAddrs(t, alice2),
})
require.NoError(t, err)

acc1 := must.Do2(alice1.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{}))
acc2 := must.Do2(alice2.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{}))

require.False(t, proto.Equal(acc1, acc2), "accounts must not match before syncing")

{
Expand All @@ -701,14 +780,82 @@ func TestMultiDevice(t *testing.T) {
require.Equal(t, int64(0), sr.NumSyncFailed)
require.Equal(t, []peer.ID{alice2.Storage.Device().PeerID(), alice1.Storage.Device().PeerID()}, sr.Peers)
}

acc1 = must.Do2(alice1.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{}))
acc2 = must.Do2(alice2.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{}))
testutil.ProtoEqual(t, acc1, acc2, "accounts must match after sync")

require.Len(t, acc2.Devices, 2, "must have two devices after syncing")
}

func TestTrustedPeers(t *testing.T) {
t.Parallel()

alice := makeTestApp(t, "alice", makeTestConfig(t), true)
bob := makeTestApp(t, "bob", makeTestConfig(t), true)
ctx := context.Background()

_, err := alice.RPC.Networking.Connect(ctx, &networking.ConnectRequest{
Addrs: getAddrs(t, bob),
})
require.NoError(t, err)

{
sr := must.Do2(alice.Syncing.MustGet().Sync(ctx))
require.Equal(t, int64(1), sr.NumSyncOK)
require.Equal(t, int64(0), sr.NumSyncFailed)
require.Equal(t, []peer.ID{alice.Storage.Device().PeerID(), bob.Storage.Device().PeerID()}, sr.Peers)
}

{
sr := must.Do2(bob.Syncing.MustGet().Sync(ctx))
require.Equal(t, int64(1), sr.NumSyncOK)
require.Equal(t, int64(0), sr.NumSyncFailed)
require.Equal(t, []peer.ID{bob.Storage.Device().PeerID(), alice.Storage.Device().PeerID()}, sr.Peers)
}

acc1 := must.Do2(alice.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{Id: bob.Net.MustGet().ID().Account().Principal().String()}))
require.False(t, acc1.IsTrusted)
acc2 := must.Do2(bob.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{Id: alice.Net.MustGet().ID().Account().Principal().String()}))
require.False(t, acc2.IsTrusted)

acc1, err = alice.RPC.Accounts.SetAccountTrust(ctx, &accounts.SetAccountTrustRequest{Id: bob.Net.MustGet().ID().Account().Principal().String(), IsTrusted: true})
require.NoError(t, err)
require.True(t, acc1.IsTrusted)

//Just because they sync the should not be trusted
{
sr := must.Do2(alice.Syncing.MustGet().Sync(ctx))
require.Equal(t, int64(1), sr.NumSyncOK)
require.Equal(t, int64(0), sr.NumSyncFailed)
require.Equal(t, []peer.ID{alice.Storage.Device().PeerID(), bob.Storage.Device().PeerID()}, sr.Peers)
}

{
sr := must.Do2(bob.Syncing.MustGet().Sync(ctx))
require.Equal(t, int64(1), sr.NumSyncOK)
require.Equal(t, int64(0), sr.NumSyncFailed)
require.Equal(t, []peer.ID{bob.Storage.Device().PeerID(), alice.Storage.Device().PeerID()}, sr.Peers)
}
time.Sleep(100 * time.Millisecond) // to give time to sync
acc1 = must.Do2(alice.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{Id: bob.Net.MustGet().ID().Account().Principal().String()}))
require.True(t, acc1.IsTrusted)
acc2 = must.Do2(bob.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{Id: alice.Net.MustGet().ID().Account().Principal().String()}))
require.False(t, acc2.IsTrusted)

acc1, err = alice.RPC.Accounts.SetAccountTrust(ctx, &accounts.SetAccountTrustRequest{Id: bob.Net.MustGet().ID().Account().Principal().String(), IsTrusted: false})
require.NoError(t, err)
require.False(t, acc1.IsTrusted)
acc2, err = bob.RPC.Accounts.SetAccountTrust(ctx, &accounts.SetAccountTrustRequest{Id: alice.Net.MustGet().ID().Account().Principal().String(), IsTrusted: true})
require.NoError(t, err)
require.True(t, acc2.IsTrusted)

//Double check
acc1 = must.Do2(alice.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{Id: bob.Net.MustGet().ID().Account().Principal().String()}))
require.False(t, acc1.IsTrusted)
acc2 = must.Do2(bob.RPC.Accounts.GetAccount(ctx, &accounts.GetAccountRequest{Id: alice.Net.MustGet().ID().Account().Principal().String()}))
require.True(t, acc2.IsTrusted)
}

func TestNetworkingListPeers(t *testing.T) {
t.Parallel()

Expand Down
Loading

0 comments on commit aca3996

Please sign in to comment.