diff options
| author | 2022-03-15 16:12:35 +0100 | |
|---|---|---|
| committer | 2022-03-15 16:12:35 +0100 | |
| commit | 532c4cc6978a7fe707373106eebade237c89693a (patch) | |
| tree | 937bf0a44ef2a8d8d366786693decf2c65b20db5 /internal/processing | |
| parent | [performance] Add dereference shortcuts to avoid making http calls to self (#... (diff) | |
| download | gotosocial-532c4cc6978a7fe707373106eebade237c89693a.tar.xz | |
[feature] Federate local account deletion (#431)
* add account delete to API
* model account delete request
* add AccountDeleteLocal
* federate local account deletes
* add DeleteLocal
* update transport (controller) to allow shortcuts
* delete logic + testing
* update swagger docs
* more tests + fixes
Diffstat (limited to 'internal/processing')
| -rw-r--r-- | internal/processing/account.go | 4 | ||||
| -rw-r--r-- | internal/processing/account/account.go | 5 | ||||
| -rw-r--r-- | internal/processing/account/delete.go | 53 | ||||
| -rw-r--r-- | internal/processing/account_test.go | 90 | ||||
| -rw-r--r-- | internal/processing/fromclientapi.go | 59 | ||||
| -rw-r--r-- | internal/processing/processor.go | 2 | ||||
| -rw-r--r-- | internal/processing/processor_test.go | 2 | 
7 files changed, 210 insertions, 5 deletions
diff --git a/internal/processing/account.go b/internal/processing/account.go index a93d8ba12..80f6604fe 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -30,6 +30,10 @@ func (p *processor) AccountCreate(ctx context.Context, authed *oauth.Auth, form  	return p.accountProcessor.Create(ctx, authed.Token, authed.Application, form)  } +func (p *processor) AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode { +	return p.accountProcessor.DeleteLocal(ctx, authed.Account, form) +} +  func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) {  	return p.accountProcessor.Get(ctx, authed.Account, targetAccountID)  } diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index b2321f414..c670ee24a 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -42,7 +42,10 @@ type Processor interface {  	Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error)  	// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.  	// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block. -	Delete(ctx context.Context, account *gtsmodel.Account, origin string) error +	Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode +	// DeleteLocal is like delete, but specifically for deletion of local accounts rather than federated ones. +	// Unlike Delete, it will propagate the deletion out across the federating API to other instances. +	DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode  	// Get processes the given request for account information.  	Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error)  	// Update processes the update of an account with the given form diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index 44429822e..d15c4858c 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -20,13 +20,17 @@ package account  import (  	"context" +	"errors"  	"time"  	"github.com/sirupsen/logrus"  	"github.com/superseriousbusiness/gotosocial/internal/ap" +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/messages" +	"golang.org/x/crypto/bcrypt"  )  // Delete handles the complete deletion of an account. @@ -50,7 +54,7 @@ import (  // 16. Delete account's user  // 17. Delete account's timeline  // 18. Delete account itself -func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) error { +func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode {  	fields := logrus.Fields{  		"func":     "Delete",  		"username": account.Username, @@ -256,7 +260,7 @@ selectStatusesLoop:  	// 16. Delete account's user  	l.Debug("deleting account user")  	if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, >smodel.User{}); err != nil { -		return err +		return gtserror.NewErrorInternalError(err)  	}  	// 17. Delete account's timeline @@ -282,9 +286,52 @@ selectStatusesLoop:  	account, err := p.db.UpdateAccount(ctx, account)  	if err != nil { -		return err +		return gtserror.NewErrorInternalError(err)  	}  	l.Infof("deleted account with username %s from domain %s", account.Username, account.Domain)  	return nil  } + +func (p *processor) DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode { +	fromClientAPIMessage := messages.FromClientAPI{ +		APObjectType:   ap.ActorPerson, +		APActivityType: ap.ActivityDelete, +		TargetAccount:  account, +	} + +	if form.DeleteOriginID == account.ID { +		// the account owner themself has requested deletion via the API, get their user from the db +		user := >smodel.User{} +		if err := p.db.GetWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, user); err != nil { +			return gtserror.NewErrorInternalError(err) +		} + +		// now check that the password they supplied is correct +		// make sure a password is actually set and bail if not +		if user.EncryptedPassword == "" { +			return gtserror.NewErrorForbidden(errors.New("user password was not set")) +		} + +		// compare the provided password with the encrypted one from the db, bail if they don't match +		if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(form.Password)); err != nil { +			return gtserror.NewErrorForbidden(errors.New("invalid password")) +		} + +		fromClientAPIMessage.OriginAccount = account +	} else { +		// the delete has been requested by some other account, grab it; +		// if we've reached this point we know it has permission already +		requestingAccount, err := p.db.GetAccountByID(ctx, form.DeleteOriginID) +		if err != nil { +			return gtserror.NewErrorInternalError(err) +		} + +		fromClientAPIMessage.OriginAccount = requestingAccount +	} + +	// put the delete in the processor queue to handle the rest of it asynchronously +	p.fromClientAPI <- fromClientAPIMessage + +	return nil +} diff --git a/internal/processing/account_test.go b/internal/processing/account_test.go new file mode 100644 index 000000000..f974f0b9d --- /dev/null +++ b/internal/processing/account_test.go @@ -0,0 +1,90 @@ +/* +   GoToSocial +   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package processing_test + +import ( +	"context" +	"encoding/json" +	"fmt" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/activity/pub" +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type AccountTestSuite struct { +	ProcessingStandardTestSuite +} + +func (suite *AccountTestSuite) TestAccountDeleteLocal() { +	ctx := context.Background() +	deletingAccount := suite.testAccounts["local_account_1"] +	followingAccount := suite.testAccounts["remote_account_1"] + +	// make the following account follow the deleting account so that a delete message will be sent to it via the federating API +	follow := >smodel.Follow{ +		ID:              "01FJ1S8DX3STJJ6CEYPMZ1M0R3", +		CreatedAt:       time.Now(), +		UpdatedAt:       time.Now(), +		URI:             fmt.Sprintf("%s/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3", followingAccount.URI), +		AccountID:       followingAccount.ID, +		TargetAccountID: deletingAccount.ID, +	} +	err := suite.db.Put(ctx, follow) +	suite.NoError(err) + +	errWithCode := suite.processor.AccountDeleteLocal(ctx, suite.testAutheds["local_account_1"], &apimodel.AccountDeleteRequest{ +		Password:       "password", +		DeleteOriginID: deletingAccount.ID, +	}) +	suite.NoError(errWithCode) +	time.Sleep(1 * time.Second) // wait a sec for the delete to process + +	// the delete should be federated outwards to the following account's inbox +	sent, ok := suite.sentHTTPRequests[followingAccount.InboxURI] +	suite.True(ok) +	delete := &struct { +		Actor  string `json:"actor"` +		ID     string `json:"id"` +		Object string `json:"object"` +		To     string `json:"to"` +		CC     string `json:"cc"` +		Type   string `json:"type"` +	}{} +	err = json.Unmarshal(sent, delete) +	suite.NoError(err) + +	suite.Equal(deletingAccount.URI, delete.Actor) +	suite.Equal(deletingAccount.URI, delete.Object) +	suite.Equal(deletingAccount.FollowersURI, delete.To) +	suite.Equal(pub.PublicActivityPubIRI, delete.CC) +	suite.Equal("Delete", delete.Type) + +	// the deleted account should be deleted +	dbAccount, err := suite.db.GetAccountByID(ctx, deletingAccount.ID) +	suite.NoError(err) +	suite.WithinDuration(dbAccount.SuspendedAt, time.Now(), 30*time.Second) +} + +func TestAccountTestSuite(t *testing.T) { +	suite.Run(t, &AccountTestSuite{}) +} diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 11ce2215e..4935f5627 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -24,6 +24,7 @@ import (  	"fmt"  	"net/url" +	"github.com/superseriousbusiness/activity/pub"  	"github.com/superseriousbusiness/activity/streams"  	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/db" @@ -320,11 +321,69 @@ func (p *processor) processDeleteAccountFromClientAPI(ctx context.Context, clien  		// origin is whichever account caused this message  		origin = clientMsg.OriginAccount.ID  	} + +	if err := p.federateAccountDelete(ctx, clientMsg.TargetAccount); err != nil { +		return err +	} +  	return p.accountProcessor.Delete(ctx, clientMsg.TargetAccount, origin)  }  // TODO: move all the below functions into federation.Federator +func (p *processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error { +	// do nothing if this isn't our account +	if account.Domain != "" { +		return nil +	} + +	outboxIRI, err := url.Parse(account.OutboxURI) +	if err != nil { +		return fmt.Errorf("federateAccountDelete: error parsing outboxURI %s: %s", account.OutboxURI, err) +	} + +	actorIRI, err := url.Parse(account.URI) +	if err != nil { +		return fmt.Errorf("federateAccountDelete: error parsing actorIRI %s: %s", account.URI, err) +	} + +	followersIRI, err := url.Parse(account.FollowersURI) +	if err != nil { +		return fmt.Errorf("federateAccountDelete: error parsing followersIRI %s: %s", account.FollowersURI, err) +	} + +	publicIRI, err := url.Parse(pub.PublicActivityPubIRI) +	if err != nil { +		return fmt.Errorf("federateAccountDelete: error parsing url %s: %s", pub.PublicActivityPubIRI, err) +	} + +	// create a delete and set the appropriate actor on it +	delete := streams.NewActivityStreamsDelete() + +	// set the actor for the delete; no matter who deleted it we should use the account owner for this +	deleteActor := streams.NewActivityStreamsActorProperty() +	deleteActor.AppendIRI(actorIRI) +	delete.SetActivityStreamsActor(deleteActor) + +	// Set the account IRI as the 'object' property. +	deleteObject := streams.NewActivityStreamsObjectProperty() +	deleteObject.AppendIRI(actorIRI) +	delete.SetActivityStreamsObject(deleteObject) + +	// send to followers... +	deleteTo := streams.NewActivityStreamsToProperty() +	deleteTo.AppendIRI(followersIRI) +	delete.SetActivityStreamsTo(deleteTo) + +	// ... and CC to public +	deleteCC := streams.NewActivityStreamsCcProperty() +	deleteCC.AppendIRI(publicIRI) +	delete.SetActivityStreamsCc(deleteCC) + +	_, err = p.federator.FederatingActor().Send(ctx, outboxIRI, delete) +	return err +} +  func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error {  	// do nothing if the status shouldn't be federated  	if !status.Federated { diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 3db3d77c9..973b44084 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -73,6 +73,8 @@ type Processor interface {  	// AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful.  	AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) +	// AccountDeleteLocal processes the delete of a LOCAL account using the given form. +	AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode  	// AccountGet processes the given request for account information.  	AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error)  	// AccountUpdate processes the update of an account with the given form diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index 794bcc197..40a4ecba6 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -95,8 +95,8 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() {  }  func (suite *ProcessingStandardTestSuite) SetupTest() { -	testrig.InitTestLog()  	testrig.InitTestConfig() +	testrig.InitTestLog()  	suite.db = testrig.NewTestDB()  	suite.testActivities = testrig.NewTestActivities(suite.testAccounts)  | 
