Skip to content

Commit

Permalink
Feature: Remove users access from a shared file (#269)
Browse files Browse the repository at this point in the history
* Feature: Remove users access from a shared file

* DeleteReceivedFiles when access to file is revoked
  • Loading branch information
perfectmak authored Dec 3, 2020
1 parent a187b50 commit 51f4c46
Show file tree
Hide file tree
Showing 16 changed files with 1,347 additions and 673 deletions.
26 changes: 22 additions & 4 deletions core/space/domain/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const (
INVITATION
USAGEALERT
INVITATION_REPLY
REVOKED_INVITATION
)

type FullPath struct {
Expand Down Expand Up @@ -125,6 +126,14 @@ type InvitationReply struct {
InvitationID string `json:"invitationID"`
}

// Represents when an inviter unshared access to previously shared files in ItemPaths
type RevokedInvitation struct {
InviterPublicKey string `json:"inviterPublicKey"`
InviteePublicKey string `json:"inviteePublicKey"`
ItemPaths []FullPath `json:"itemPaths"`
Keys [][]byte `json:"keys"`
}

type UsageAlert struct {
Used int64 `json:"used"`
Limit int64 `json:"limit"`
Expand All @@ -144,10 +153,11 @@ type Notification struct {
CreatedAt int64 `json:"createdAt"`
ReadAt int64 `json:"readAt"`
// QUESTION: is there a way to enforce that only one of the below is present
InvitationValue Invitation `json:"invitationValue"`
UsageAlertValue UsageAlert `json:"usageAlertValue"`
InvitationAcceptValue InvitationReply `json:"invitationAcceptValue"`
RelatedObject interface{} `json:"relatedObject"`
InvitationValue Invitation `json:"invitationValue"`
UsageAlertValue UsageAlert `json:"usageAlertValue"`
InvitationAcceptValue InvitationReply `json:"invitationAcceptValue"`
RevokedInvitationValue RevokedInvitation `json:"revokedInvitationValue"`
RelatedObject interface{} `json:"relatedObject"`
}

type APISessionTokens struct {
Expand Down Expand Up @@ -201,3 +211,11 @@ func (b KeyBackupType) String() string {
return fmt.Sprintf("%d", int(b))
}
}

// SharedFilesRoleAction represents action to be performed on the role
type SharedFilesRoleAction int

const (
DeleteRoleAction SharedFilesRoleAction = iota
ReadWriteRoleAction
)
121 changes: 103 additions & 18 deletions core/space/services/services_sharing.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/FleekHQ/space-daemon/config"

"github.com/FleekHQ/space-daemon/log"
crypto "github.com/libp2p/go-libp2p-crypto"
"github.com/libp2p/go-libp2p-core/crypto"
"github.com/pkg/errors"

"github.com/FleekHQ/space-daemon/core/space/domain"
Expand Down Expand Up @@ -311,6 +311,70 @@ func (s *Space) ShareFilesViaPublicKey(ctx context.Context, paths []domain.FullP
return err
}

enhancedPaths, enckeys, err := s.resolveFullPaths(ctx, paths)
if err != nil {
return err
}

for i, path := range enhancedPaths {
_, err = s.tc.GetModel().CreateSentFileViaInvitation(ctx, path, "", enckeys[i])
if err != nil {
return err
}
}

err = s.tc.ManageShareFilesViaPublicKey(ctx, enhancedPaths, pubkeys, enckeys, domain.ReadWriteRoleAction)
if err != nil {
return err
}

for _, pk := range pubkeys {
inviter, err := s.keychain.GetStoredPublicKey()
if err != nil {
return err
}

inviterRaw, err := inviter.Raw()
if err != nil {
return err
}

pkRaw, err := pk.Raw()
if err != nil {
return err
}

d := &domain.Invitation{
InviterPublicKey: hex.EncodeToString(inviterRaw),
InviteePublicKey: hex.EncodeToString(pkRaw),
ItemPaths: enhancedPaths,
Keys: enckeys,
}

i, err := json.Marshal(d)
if err != nil {
return err
}

b := &domain.MessageBody{
Type: domain.INVITATION,
Body: i,
}

j, err := json.Marshal(b)
if err != nil {
return err
}

_, err = s.tc.SendMessage(ctx, pk, j)
if err != nil {
return err
}
}
return nil
}

func (s *Space) resolveFullPaths(ctx context.Context, paths []domain.FullPath) ([]domain.FullPath, [][]byte, error) {
m := s.tc.GetModel()

enhancedPaths := make([]domain.FullPath, len(paths))
Expand All @@ -328,12 +392,12 @@ func (s *Space) ShareFilesViaPublicKey(ctx context.Context, paths []domain.FullP
if ep.DbId == "" {
b, err := s.tc.GetDefaultBucket(ctx)
if err != nil {
return err
return nil, nil, err
}

bs, err := m.FindBucket(ctx, b.Slug())
if err != nil {
return err
return nil, nil, err
}

ep.DbId = bs.RemoteDbID
Expand All @@ -342,45 +406,63 @@ func (s *Space) ShareFilesViaPublicKey(ctx context.Context, paths []domain.FullP
if ep.Bucket == "" || ep.Bucket == t.GetDefaultBucketSlug() {
b, err := s.tc.GetDefaultBucket(ctx)
if err != nil {
return err
return nil, nil, err
}
bs, err := m.FindBucket(ctx, b.GetData().Name)
if err != nil {
return err
return nil, nil, err
}
ep.Bucket = t.GetDefaultMirrorBucketSlug()
ep.BucketKey = bs.RemoteBucketKey
enckeys[i] = bs.EncryptionKey
} else {
r, err := m.FindReceivedFile(ctx, path.DbId, path.Bucket, path.Path)
if err != nil {
return err
return nil, nil, err
}
ep.Bucket = r.Bucket
ep.BucketKey = r.BucketKey
enckeys[i] = r.EncryptionKey
}

_, err = m.CreateSentFileViaInvitation(ctx, ep, "", enckeys[i])
if err != nil {
return err
}

enhancedPaths[i] = ep
}

err = s.tc.ShareFilesViaPublicKey(ctx, enhancedPaths, pubkeys, enckeys)
return enhancedPaths, enckeys, nil
}

func (s *Space) UnshareFilesViaPublicKey(ctx context.Context, paths []domain.FullPath, pubkeys []crypto.PubKey) error {
err := s.waitForTextileHub(ctx)
if err != nil {
return err
}

enhancedPaths, enckeys, err := s.resolveFullPaths(ctx, paths)
if err != nil {
return err
}

err = s.tc.ManageShareFilesViaPublicKey(ctx, enhancedPaths, pubkeys, enckeys, domain.DeleteRoleAction)
if err != nil {
return err
}

return s.sendPathsRevokedInvitation(ctx, pubkeys, enhancedPaths, enckeys)
}

func (s *Space) sendPathsRevokedInvitation(
ctx context.Context,
pubkeys []crypto.PubKey,
enhancedPaths []domain.FullPath,
keys [][]byte,
) error {
for _, pk := range pubkeys {
inviter, err := s.keychain.GetStoredPublicKey()
uninviter, err := s.keychain.GetStoredPublicKey()
if err != nil {
return err
}

inviterRaw, err := inviter.Raw()
rawUniviter, err := uninviter.Raw()
if err != nil {
return err
}
Expand All @@ -390,11 +472,11 @@ func (s *Space) ShareFilesViaPublicKey(ctx context.Context, paths []domain.FullP
return err
}

d := &domain.Invitation{
InviterPublicKey: hex.EncodeToString(inviterRaw),
d := &domain.RevokedInvitation{
InviterPublicKey: hex.EncodeToString(rawUniviter),
InviteePublicKey: hex.EncodeToString(pkRaw),
ItemPaths: enhancedPaths,
Keys: enckeys,
Keys: keys,
}

i, err := json.Marshal(d)
Expand All @@ -403,7 +485,7 @@ func (s *Space) ShareFilesViaPublicKey(ctx context.Context, paths []domain.FullP
}

b := &domain.MessageBody{
Type: domain.INVITATION,
Type: domain.REVOKED_INVITATION,
Body: i,
}

Expand Down Expand Up @@ -448,6 +530,9 @@ func (s *Space) HandleSharedFilesInvitation(ctx context.Context, invitationId st

if accept {
invitation, err = s.tc.AcceptSharedFilesInvitation(ctx, invitation)
if err != nil {
return err
}

// notify inviter, it was accepted
invitersPk, err := decodePublicKey(err, invitation.InviterPublicKey)
Expand Down
3 changes: 2 additions & 1 deletion core/space/space.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/FleekHQ/space-daemon/core/permissions"
"github.com/FleekHQ/space-daemon/core/textile/hub"
"github.com/FleekHQ/space-daemon/core/vault"
crypto "github.com/libp2p/go-libp2p-crypto"
"github.com/libp2p/go-libp2p-core/crypto"

"github.com/FleekHQ/space-daemon/config"
"github.com/FleekHQ/space-daemon/core/env"
Expand Down Expand Up @@ -53,6 +53,7 @@ type Service interface {
ToggleBucketBackup(ctx context.Context, bucketSlug string, bucketBackup bool) error
BucketBackupRestore(ctx context.Context, bucketSlug string) error
ShareFilesViaPublicKey(ctx context.Context, paths []domain.FullPath, pubkeys []crypto.PubKey) error
UnshareFilesViaPublicKey(ctx context.Context, paths []domain.FullPath, pks []crypto.PubKey) error
HandleSharedFilesInvitation(ctx context.Context, invitationId string, accept bool) error
GetAPISessionTokens(ctx context.Context) (*domain.APISessionTokens, error)
AddRecentlySharedPublicKeys(ctx context.Context, pubkeys []crypto.PubKey) error
Expand Down
35 changes: 35 additions & 0 deletions core/space/space_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,41 @@ func TestService_VaultRestore(t *testing.T) {
mockKeychain.AssertCalled(t, "ImportExistingKeyPair", mockPrivKey, mnemonic)
}

func TestService_UnshareFilesViaPublicKey_Works(t *testing.T) {
sv, _, tearDown := initTestService(t)
defer tearDown()
ctx := context.Background()

textileClient.On("IsHealthy").Return(true)
textileClient.On("GetModel").Return(mockModel)
textileClient.On(
"ManageShareFilesViaPublicKey",
ctx,
[]domain.FullPath{},
[]crypto.PubKey{},
[][]byte{},
domain.DeleteRoleAction,
).Return(nil)

err := sv.UnshareFilesViaPublicKey(ctx, []domain.FullPath{}, []crypto.PubKey{})
assert.Nil(t, err)
}

func TestService_UnshareFilesViaPublicKey_Fails_IFTextileIsNotInitialized(t *testing.T) {
sv, _, tearDown := initTestService(t)
defer tearDown()
ctx := context.Background()
expectedErr := errors.New("textile is not initialized")
errChan := make(chan error, 1)
errChan <- expectedErr

textileClient.On("WaitForHealthy").Return(errChan)
textileClient.On("IsHealthy").Return(false)

err := sv.UnshareFilesViaPublicKey(ctx, []domain.FullPath{}, []crypto.PubKey{})
assert.EqualError(t, err, expectedErr.Error())
}

func TestService_HandleSharedFilesInvitation_FailIfInvitationNotFound(t *testing.T) {
sv, _, tearDown := initTestService(t)
ctx := context.Background()
Expand Down
17 changes: 17 additions & 0 deletions core/textile/mailbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ func (tc *textileClient) parseMessage(ctx context.Context, msgs []client.Message
}
n.UsageAlertValue = *u
n.RelatedObject = *u
case domain.REVOKED_INVITATION:
invite := domain.RevokedInvitation{}
if err := json.Unmarshal((*b).Body, &invite); err != nil {
return nil, err
}

// NOTE: current, this would run every time this notification is fetched
// we can further optimize this later to prevent unnecessary calls, but for now
// it would run asynchronously.
go func() {
if err = tc.GetModel().DeleteReceivedFiles(ctx, invite.ItemPaths, invite.Keys); err != nil {
log.Error("Failed to delete revoked files", err)
}
}()

n.RevokedInvitationValue = invite
n.RelatedObject = invite
default:
}

Expand Down
1 change: 1 addition & 0 deletions core/textile/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type Model interface {
ListReceivedFiles(ctx context.Context, accepted bool, seek string, limit int) ([]*ReceivedFileSchema, error)
ListSentFiles(ctx context.Context, seek string, limit int) ([]*SentFileSchema, error)
ListReceivedPublicFiles(ctx context.Context, cidHash string, accepted bool) ([]*ReceivedFileSchema, error)
DeleteReceivedFiles(ctx context.Context, paths []domain.FullPath, keys [][]byte) error
FindMirrorFileByPaths(ctx context.Context, paths []string) (map[string]*MirrorFileSchema, error)
FindReceivedFilesByIds(ctx context.Context, ids []string) ([]*ReceivedFileSchema, error)
InitSearchIndexCollection(ctx context.Context) error
Expand Down
42 changes: 42 additions & 0 deletions core/textile/model/received_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,48 @@ func (m *model) ListReceivedFiles(ctx context.Context, accepted bool, seek strin
return files, nil
}

func (m *model) DeleteReceivedFiles(ctx context.Context, paths []domain.FullPath, keys [][]byte) error {
if len(paths) == 0 {
return nil
}

metaCtx, dbID, err := m.initReceivedFileModel(ctx)
if err != nil || dbID == nil {
return err
}

// build find query
var findQuery *db.Query
for i, path := range paths {
q := db.Where("dbId").Eq(path.DbId).
And("bucket").Eq(path.BucketKey).
And("path").Eq(path.Path).
And("encryptionKey").Eq(keys[i])
if findQuery == nil {
findQuery = q
} else {
findQuery = findQuery.Or(q)
}
}

rawFiles, err := m.threads.Find(metaCtx, *dbID, receivedFileModelName, findQuery, &ReceivedFileSchema{})
if err != nil {
return err
}
if rawFiles == nil {
return nil
}

// extract instance ids from result
files := rawFiles.([]*ReceivedFileSchema)
instanceIds := make([]string, len(files))
for i, file := range files {
instanceIds[i] = file.ID.String()
}

return m.threads.Delete(metaCtx, *dbID, receivedFileModelName, instanceIds)
}

func (m *model) ListReceivedPublicFiles(
ctx context.Context,
cidHash string,
Expand Down
Loading

0 comments on commit 51f4c46

Please sign in to comment.