[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
This commit is contained in:
tobi
2022-03-15 16:12:35 +01:00
committed by GitHub
parent e63b653199
commit 532c4cc697
15 changed files with 541 additions and 16 deletions

View File

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

View File

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

View File

@@ -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
}

View File

@@ -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{})
}

View File

@@ -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 {

View File

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

View File

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