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) |