summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2022-03-15 16:12:35 +0100
committerLibravatar GitHub <noreply@github.com>2022-03-15 16:12:35 +0100
commit532c4cc6978a7fe707373106eebade237c89693a (patch)
tree937bf0a44ef2a8d8d366786693decf2c65b20db5 /internal/processing
parent[performance] Add dereference shortcuts to avoid making http calls to self (#... (diff)
downloadgotosocial-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.go4
-rw-r--r--internal/processing/account/account.go5
-rw-r--r--internal/processing/account/delete.go53
-rw-r--r--internal/processing/account_test.go90
-rw-r--r--internal/processing/fromclientapi.go59
-rw-r--r--internal/processing/processor.go2
-rw-r--r--internal/processing/processor_test.go2
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}}, &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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)