mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-25 08:30:29 +00:00
[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:
parent
e63b653199
commit
532c4cc697
15 changed files with 541 additions and 16 deletions
|
@ -1798,7 +1798,7 @@ info:
|
|||
name: AGPL3
|
||||
url: https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
title: GoToSocial
|
||||
version: 0.0.1
|
||||
version: REPLACE_ME
|
||||
paths:
|
||||
/.well-known/nodeinfo:
|
||||
get:
|
||||
|
@ -2191,6 +2191,31 @@ paths:
|
|||
summary: Unfollow account with id.
|
||||
tags:
|
||||
- accounts
|
||||
/api/v1/accounts/delete:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
operationId: accountDelete
|
||||
parameters:
|
||||
- description: Password of the account user, for confirmation.
|
||||
in: formData
|
||||
name: password
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"202":
|
||||
description: The account deletion has been accepted and the account will
|
||||
be deleted.
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:accounts
|
||||
summary: Delete your account.
|
||||
tags:
|
||||
- accounts
|
||||
/api/v1/accounts/relationships:
|
||||
get:
|
||||
operationId: accountRelationships
|
||||
|
@ -2341,6 +2366,8 @@ paths:
|
|||
description: bad request
|
||||
"403":
|
||||
description: forbidden
|
||||
"409":
|
||||
description: conflict -- domain/shortcode combo for emoji already exists
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
|
|
|
@ -71,6 +71,8 @@ const (
|
|||
BlockPath = BasePathWithID + "/block"
|
||||
// UnblockPath is for removing a block of an account
|
||||
UnblockPath = BasePathWithID + "/unblock"
|
||||
// DeleteAccountPath is for deleting one's account via the API
|
||||
DeleteAccountPath = BasePath + "/delete"
|
||||
)
|
||||
|
||||
// Module implements the ClientAPIModule interface for account-related actions
|
||||
|
@ -90,6 +92,9 @@ func (m *Module) Route(r router.Router) error {
|
|||
// create account
|
||||
r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler)
|
||||
|
||||
// delete account
|
||||
r.AttachHandler(http.MethodPost, DeleteAccountPath, m.AccountDeletePOSTHandler)
|
||||
|
||||
// get account
|
||||
r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
|
@ -27,7 +26,6 @@ type AccountStandardTestSuite struct {
|
|||
// standard suite interfaces
|
||||
suite.Suite
|
||||
db db.DB
|
||||
tc typeutils.TypeConverter
|
||||
storage *kv.KVStore
|
||||
mediaManager media.Manager
|
||||
federator federation.Federator
|
||||
|
|
90
internal/api/client/account/accountdelete.go
Normal file
90
internal/api/client/account/accountdelete.go
Normal 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 account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// AccountDeletePOSTHandler swagger:operation POST /api/v1/accounts/delete accountDelete
|
||||
//
|
||||
// Delete your account.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - accounts
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
//
|
||||
// parameters:
|
||||
// - name: password
|
||||
// in: formData
|
||||
// description: Password of the account user, for confirmation.
|
||||
// type: string
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:accounts
|
||||
//
|
||||
// responses:
|
||||
// '202':
|
||||
// description: "The account deletion has been accepted and the account will be deleted."
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
|
||||
l := logrus.WithField("func", "AccountDeletePOSTHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("couldn't auth: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
l.Tracef("retrieved account %+v", authed.Account.ID)
|
||||
|
||||
form := &model.AccountDeleteRequest{}
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if form.Password == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no password provided in account delete request"})
|
||||
return
|
||||
}
|
||||
|
||||
form.DeleteOriginID = authed.Account.ID
|
||||
|
||||
if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil {
|
||||
l.Debugf("could not delete account: %s", errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{"message": "accepted"})
|
||||
}
|
101
internal/api/client/account/accountdelete_test.go
Normal file
101
internal/api/client/account/accountdelete_test.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
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 account_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/account"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type AccountDeleteTestSuite struct {
|
||||
AccountStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandler() {
|
||||
// set up the request
|
||||
// we're deleting zork
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{
|
||||
"password": "password",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType())
|
||||
|
||||
// call the handler
|
||||
suite.accountModule.AccountDeletePOSTHandler(ctx)
|
||||
|
||||
// 1. we should have Accepted because our request was valid
|
||||
suite.Equal(http.StatusAccepted, recorder.Code)
|
||||
}
|
||||
|
||||
func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerWrongPassword() {
|
||||
// set up the request
|
||||
// we're deleting zork
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{
|
||||
"password": "aaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType())
|
||||
|
||||
// call the handler
|
||||
suite.accountModule.AccountDeletePOSTHandler(ctx)
|
||||
|
||||
// 1. we should have Forbidden because we supplied the wrong password
|
||||
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||
}
|
||||
|
||||
func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerNoPassword() {
|
||||
// set up the request
|
||||
// we're deleting zork
|
||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||
"", "",
|
||||
map[string]string{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := requestBody.Bytes()
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType())
|
||||
|
||||
// call the handler
|
||||
suite.accountModule.AccountDeletePOSTHandler(ctx)
|
||||
|
||||
// 1. we should have StatusBadRequest because our request was invalid
|
||||
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||
}
|
||||
|
||||
func TestAccountDeleteTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(AccountDeleteTestSuite))
|
||||
}
|
|
@ -187,3 +187,14 @@ type AccountFollowRequest struct {
|
|||
// Notify when this account posts.
|
||||
Notify *bool `form:"notify" json:"notify" xml:"notify"`
|
||||
}
|
||||
|
||||
// AccountDeleteRequest models a request to delete an account.
|
||||
//
|
||||
// swagger:ignore
|
||||
type AccountDeleteRequest struct {
|
||||
// Password of the account's user, for confirmation.
|
||||
Password string `form:"password" json:"password" xml:"password"`
|
||||
// The origin of the delete account request.
|
||||
// Can be the ID of the account owner, or the ID of an admin account.
|
||||
DeleteOriginID string `form:"-" json:"-" xml:"-"`
|
||||
}
|
||||
|
|
|
@ -25,13 +25,15 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
|
@ -80,24 +82,101 @@ func (suite *UserGetTestSuite) TestGetUser() {
|
|||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
// should be a Person
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
assert.NoError(suite.T(), err)
|
||||
suite.NoError(err)
|
||||
|
||||
person, ok := t.(vocab.ActivityStreamsPerson)
|
||||
assert.True(suite.T(), ok)
|
||||
suite.True(ok)
|
||||
|
||||
// convert person to account
|
||||
// since this account is already known, we should get a pretty full model of it from the conversion
|
||||
a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, false)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.EqualValues(suite.T(), targetAccount.Username, a.Username)
|
||||
suite.NoError(err)
|
||||
suite.EqualValues(targetAccount.Username, a.Username)
|
||||
}
|
||||
|
||||
// TestGetUserPublicKeyDeleted checks whether the public key of a deleted account can still be dereferenced.
|
||||
// This is needed by remote instances for authenticating delete requests and stuff like that.
|
||||
func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// first delete the account, as though zork had deleted himself
|
||||
authed := &oauth.Auth{
|
||||
Application: suite.testApplications["local_account_1"],
|
||||
User: suite.testUsers["local_account_1"],
|
||||
Account: suite.testAccounts["local_account_1"],
|
||||
}
|
||||
suite.processor.AccountDeleteLocal(context.Background(), authed, &apimodel.AccountDeleteRequest{
|
||||
Password: "password",
|
||||
DeleteOriginID: targetAccount.ID,
|
||||
})
|
||||
|
||||
// now wait just a sec for it to go through....
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_zork_public_key"]
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager)
|
||||
userModule := user.New(processor).(*user.Module)
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.PublicKeyURI, nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// we need to pass the context through signature check first to set appropriate values on it
|
||||
suite.securityModule.SignatureCheck(ctx)
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: user.UsernameKey,
|
||||
Value: targetAccount.Username,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
userModule.UsersGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
// should be a Person
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
suite.NoError(err)
|
||||
|
||||
person, ok := t.(vocab.ActivityStreamsPerson)
|
||||
suite.True(ok)
|
||||
|
||||
// convert person to account
|
||||
a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, false)
|
||||
suite.NoError(err)
|
||||
suite.EqualValues(targetAccount.Username, a.Username)
|
||||
}
|
||||
|
||||
func TestUserGetTestSuite(t *testing.T) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
90
internal/processing/account_test.go
Normal file
90
internal/processing/account_test.go
Normal 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 := >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{})
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -377,7 +377,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
|
|||
AlsoKnownAs: "",
|
||||
PrivateKey: &rsa.PrivateKey{},
|
||||
PublicKey: &rsa.PublicKey{},
|
||||
PublicKeyURI: "http://localhost:8080/users/the_mighty_zork#main-key",
|
||||
PublicKeyURI: "http://localhost:8080/users/the_mighty_zork/main-key",
|
||||
SensitizedAt: time.Time{},
|
||||
SilencedAt: time.Time{},
|
||||
SuspendedAt: time.Time{},
|
||||
|
@ -1656,6 +1656,14 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin
|
|||
DateHeader: date,
|
||||
}
|
||||
|
||||
target = URLMustParse(accounts["local_account_1"].PublicKeyURI)
|
||||
sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
|
||||
fossSatanDereferenceZorkPublicKey := ActivityWithSignature{
|
||||
SignatureHeader: sig,
|
||||
DigestHeader: digest,
|
||||
DateHeader: date,
|
||||
}
|
||||
|
||||
target = URLMustParse(statuses["local_account_1_status_1"].URI + "/replies")
|
||||
sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target)
|
||||
fossSatanDereferenceLocalAccount1Status1Replies := ActivityWithSignature{
|
||||
|
@ -1706,6 +1714,7 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin
|
|||
|
||||
return map[string]ActivityWithSignature{
|
||||
"foss_satan_dereference_zork": fossSatanDereferenceZork,
|
||||
"foss_satan_dereference_zork_public_key": fossSatanDereferenceZorkPublicKey,
|
||||
"foss_satan_dereference_local_account_1_status_1_replies": fossSatanDereferenceLocalAccount1Status1Replies,
|
||||
"foss_satan_dereference_local_account_1_status_1_replies_next": fossSatanDereferenceLocalAccount1Status1RepliesNext,
|
||||
"foss_satan_dereference_local_account_1_status_1_replies_last": fossSatanDereferenceLocalAccount1Status1RepliesLast,
|
||||
|
|
Loading…
Reference in a new issue