[bugfix] Ensure domain block side effects skipped if allow in place (blocklist mode) (#2542)

This commit is contained in:
tobi 2024-01-19 14:13:24 +01:00 committed by GitHub
parent 5ca86b1c57
commit 33dbd3ab7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 322 additions and 12 deletions

View file

@ -26,6 +26,7 @@
"codeberg.org/gruf/go-kv" "codeberg.org/gruf/go-kv"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@ -92,6 +93,15 @@ func(ctx context.Context) gtserror.MultiError {
{"actionID", actionID}, {"actionID", actionID},
}...).WithContext(ctx) }...).WithContext(ctx)
skip, err := p.skipBlockSideEffects(ctx, domain)
if err != nil {
return err
}
if skip != "" {
l.Infof("skipping domain block side effects: %s", skip)
return nil
}
l.Info("processing domain block side effects") l.Info("processing domain block side effects")
defer func() { l.Info("finished processing domain block side effects") }() defer func() { l.Info("finished processing domain block side effects") }()
@ -109,6 +119,54 @@ func(ctx context.Context) gtserror.MultiError {
return apiDomainBlock, actionID, nil return apiDomainBlock, actionID, nil
} }
// skipBlockSideEffects checks if side effects of block creation
// should be skipped for the given domain, taking account of
// instance federation mode, and existence of any allows
// which ought to "shield" this domain from being blocked.
//
// If the caller should skip, the returned string will be non-zero
// and will be set to a reason why side effects should be skipped.
//
// - blocklist mode + allow exists: "..." (skip)
// - blocklist mode + no allow: "" (don't skip)
// - allowlist mode + allow exists: "" (don't skip)
// - allowlist mode + no allow: "" (don't skip)
func (p *Processor) skipBlockSideEffects(
ctx context.Context,
domain string,
) (string, gtserror.MultiError) {
var (
skip string // Assume "" (don't skip).
errs gtserror.MultiError
)
// Never skip block side effects in allowlist mode.
fediMode := config.GetInstanceFederationMode()
if fediMode == config.InstanceFederationModeAllowlist {
return skip, errs
}
// We know we're in blocklist mode.
//
// We want to skip domain block side
// effects if an allow is already
// in place which overrides the block.
// Check if an explicit allow exists for this domain.
domainAllow, err := p.state.DB.GetDomainAllow(ctx, domain)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
errs.Appendf("error getting domain allow: %w", err)
return skip, errs
}
if domainAllow != nil {
skip = "running in blocklist mode, and an explicit allow exists for this domain"
return skip, errs
}
return skip, errs
}
// domainBlockSideEffects processes the side effects of a domain block: // domainBlockSideEffects processes the side effects of a domain block:
// //
// 1. Strip most info away from the instance entry for the domain. // 1. Strip most info away from the instance entry for the domain.

View file

@ -19,12 +19,15 @@
import ( import (
"context" "context"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -49,7 +52,10 @@ type domainPermAction struct {
// permission action on each // permission action on each
// account on the target domain. // account on the target domain.
// Eg., suite.Zero(account.SuspendedAt) // Eg., suite.Zero(account.SuspendedAt)
expected func(*gtsmodel.Account) bool expected func(
context.Context,
*gtsmodel.Account,
) bool
} }
type domainPermTest struct { type domainPermTest struct {
@ -76,6 +82,9 @@ func (suite *DomainBlockTestSuite) runDomainPermTest(t domainPermTest) {
config.SetInstanceFederationMode(t.instanceFederationMode) config.SetInstanceFederationMode(t.instanceFederationMode)
for _, action := range t.actions { for _, action := range t.actions {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Run the desired action. // Run the desired action.
var actionID string var actionID string
switch action.createOrDelete { switch action.createOrDelete {
@ -102,7 +111,7 @@ func (suite *DomainBlockTestSuite) runDomainPermTest(t domainPermTest) {
} }
for _, account := range accounts { for _, account := range accounts {
if !action.expected(account) { if !action.expected(ctx, account) {
suite.T().FailNow() suite.T().FailNow()
} }
} }
@ -193,6 +202,42 @@ func (suite *DomainBlockTestSuite) awaitAction(actionID string) {
suite.Empty(adminAction.Errors) suite.Empty(adminAction.Errors)
} }
// shortcut to look up an account
// using the Search processor.
func (suite *DomainBlockTestSuite) lookupAccount(
ctx context.Context,
requestingAccount *gtsmodel.Account,
targetAccount *gtsmodel.Account,
) (*apimodel.Account, gtserror.WithCode) {
return suite.processor.Search().Lookup(
ctx,
requestingAccount,
"@"+targetAccount.Username+"@"+targetAccount.Domain,
)
}
// shortcut to look up target account's
// statuses using the Account processor.
func (suite *DomainBlockTestSuite) getStatuses(
ctx context.Context,
requestingAccount *gtsmodel.Account,
targetAccount *gtsmodel.Account,
) (*apimodel.PageableResponse, gtserror.WithCode) {
return suite.processor.Account().StatusesGet(
ctx,
requestingAccount,
targetAccount.ID,
0, // unlimited
false, // include replies
false, // include reblogs
id.Highest, // max ID
id.Lowest, // min ID
false, // don't filter on pinned
false, // don't filter on media
false, // don't filter on public
)
}
func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() { func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() {
const domain = "fossbros-anonymous.io" const domain = "fossbros-anonymous.io"
@ -203,7 +248,7 @@ func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() {
createOrDelete: "create", createOrDelete: "create",
permissionType: gtsmodel.DomainPermissionBlock, permissionType: gtsmodel.DomainPermissionBlock,
domain: domain, domain: domain,
expected: func(account *gtsmodel.Account) bool { expected: func(_ context.Context, account *gtsmodel.Account) bool {
// Domain was blocked, so each // Domain was blocked, so each
// account should now be suspended. // account should now be suspended.
return suite.NotZero(account.SuspendedAt) return suite.NotZero(account.SuspendedAt)
@ -213,7 +258,7 @@ func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() {
createOrDelete: "delete", createOrDelete: "delete",
permissionType: gtsmodel.DomainPermissionBlock, permissionType: gtsmodel.DomainPermissionBlock,
domain: domain, domain: domain,
expected: func(account *gtsmodel.Account) bool { expected: func(_ context.Context, account *gtsmodel.Account) bool {
// Domain was unblocked, so each // Domain was unblocked, so each
// account should now be unsuspended. // account should now be unsuspended.
return suite.Zero(account.SuspendedAt) return suite.Zero(account.SuspendedAt)
@ -226,6 +271,9 @@ func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() {
func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() { func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() {
const domain = "fossbros-anonymous.io" const domain = "fossbros-anonymous.io"
// Use zork for checks within test.
var testAccount = suite.testAccounts["local_account_1"]
suite.runDomainPermTest(domainPermTest{ suite.runDomainPermTest(domainPermTest{
instanceFederationMode: config.InstanceFederationModeBlocklist, instanceFederationMode: config.InstanceFederationModeBlocklist,
actions: []domainPermAction{ actions: []domainPermAction{
@ -233,42 +281,246 @@ func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() {
createOrDelete: "create", createOrDelete: "create",
permissionType: gtsmodel.DomainPermissionBlock, permissionType: gtsmodel.DomainPermissionBlock,
domain: domain, domain: domain,
expected: func(account *gtsmodel.Account) bool { expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Domain was blocked, so each // Domain was blocked, so each
// account should now be suspended. // account should now be suspended.
return suite.NotZero(account.SuspendedAt) if account.SuspendedAt.IsZero() {
suite.T().Logf("account %s should be suspended", account.Username)
return false
}
// Local account 1 should be able to see
// no statuses from suspended account.
statuses, err := suite.getStatuses(ctx, testAccount, account)
if err != nil {
suite.FailNow(err.Error())
}
if l := len(statuses.Items); l != 0 {
suite.T().Logf("expected statuses of len 0, was %d", l)
return false
}
// Lookup for this account should return 404.
lookupAcct, err := suite.lookupAccount(ctx, testAccount, account)
if err == nil || err.Code() != http.StatusNotFound {
suite.T().Logf("expected 404 error, got %v", err)
return false
}
if lookupAcct != nil {
suite.T().Logf("expected nil account lookup, got %v", lookupAcct)
return false
}
return true
}, },
}, },
{ {
createOrDelete: "create", createOrDelete: "create",
permissionType: gtsmodel.DomainPermissionAllow, permissionType: gtsmodel.DomainPermissionAllow,
domain: domain, domain: domain,
expected: func(account *gtsmodel.Account) bool { expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Domain was explicitly allowed, so each // Domain was explicitly allowed, so each
// account should now be unsuspended, since // account should now be unsuspended, since
// the allow supercedes the block. // the allow supercedes the block.
return suite.Zero(account.SuspendedAt) if !account.SuspendedAt.IsZero() {
suite.T().Logf("account %s should not be suspended", account.Username)
return false
}
// Local account 1 should be able to see
// no statuses from account, because any
// statuses were deleted by the block above.
statuses, err := suite.getStatuses(ctx, testAccount, account)
if err != nil {
suite.FailNow(err.Error())
}
if l := len(statuses.Items); l != 0 {
suite.T().Logf("expected statuses of len 0, was %d", l)
return false
}
// Lookup for this account should return OK.
lookupAcct, err := suite.lookupAccount(ctx, testAccount, account)
if err != nil {
suite.T().Logf("expected no error, got %v", err)
return false
}
if lookupAcct == nil {
suite.T().Log("expected not nil account lookup")
return false
}
return true
}, },
}, },
{ {
createOrDelete: "delete", createOrDelete: "delete",
permissionType: gtsmodel.DomainPermissionAllow, permissionType: gtsmodel.DomainPermissionAllow,
domain: domain, domain: domain,
expected: func(account *gtsmodel.Account) bool { expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Deleting the allow now, while there's // Deleting the allow now, while there's
// still a block in place, should cause // still a block in place, should cause
// the block to take effect again. // the block to take effect again.
return suite.NotZero(account.SuspendedAt) if account.SuspendedAt.IsZero() {
suite.T().Logf("account %s should be suspended", account.Username)
return false
}
// Lookup for this account should return 404.
lookupAcct, err := suite.lookupAccount(ctx, testAccount, account)
if err == nil || err.Code() != http.StatusNotFound {
suite.T().Logf("expected 404 error, got %v", err)
return false
}
if lookupAcct != nil {
suite.T().Logf("expected nil account lookup, got %v", lookupAcct)
return false
}
return true
}, },
}, },
{ {
createOrDelete: "delete", createOrDelete: "delete",
permissionType: gtsmodel.DomainPermissionBlock, permissionType: gtsmodel.DomainPermissionBlock,
domain: domain, domain: domain,
expected: func(account *gtsmodel.Account) bool { expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Deleting the block now should // Deleting the block now should
// unsuspend the accounts again. // unsuspend the accounts again.
return suite.Zero(account.SuspendedAt) if !account.SuspendedAt.IsZero() {
suite.T().Logf("account %s should not be suspended", account.Username)
return false
}
// Lookup for this account should return OK.
lookupAcct, err := suite.lookupAccount(ctx, testAccount, account)
if err != nil {
suite.T().Logf("expected no error, got %v", err)
return false
}
if lookupAcct == nil {
suite.T().Log("expected not nil account lookup")
return false
}
return true
},
},
},
})
}
func (suite *DomainBlockTestSuite) TestAllowAndBlockDomain() {
const domain = "fossbros-anonymous.io"
// Use zork for checks within test.
var testAccount = suite.testAccounts["local_account_1"]
suite.runDomainPermTest(domainPermTest{
instanceFederationMode: config.InstanceFederationModeBlocklist,
actions: []domainPermAction{
{
createOrDelete: "create",
permissionType: gtsmodel.DomainPermissionAllow,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Domain was explicitly allowed,
// nothing should be suspended.
if !account.SuspendedAt.IsZero() {
suite.T().Logf("account %s should not be suspended", account.Username)
return false
}
// Local account 1 should be able
// to see statuses from account.
statuses, err := suite.getStatuses(ctx, testAccount, account)
if err != nil {
suite.FailNow(err.Error())
}
if l := len(statuses.Items); l == 0 {
suite.T().Log("expected some statuses, but length was 0")
return false
}
// Lookup for this account should return OK.
lookupAcct, err := suite.lookupAccount(ctx, testAccount, account)
if err != nil {
suite.T().Logf("expected no error, got %v", err)
return false
}
if lookupAcct == nil {
suite.T().Log("expected not nil account lookup")
return false
}
return true
},
},
{
createOrDelete: "create",
permissionType: gtsmodel.DomainPermissionBlock,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Create a block. An allow existed, so
// block side effects should be witheld.
// In other words, we should have the same
// results as before we added the block.
if !account.SuspendedAt.IsZero() {
suite.T().Logf("account %s should not be suspended", account.Username)
return false
}
// Local account 1 should be able
// to see statuses from account.
statuses, err := suite.getStatuses(ctx, testAccount, account)
if err != nil {
suite.FailNow(err.Error())
}
if l := len(statuses.Items); l == 0 {
suite.T().Log("expected some statuses, but length was 0")
return false
}
// Lookup for this account should return OK.
lookupAcct, err := suite.lookupAccount(ctx, testAccount, account)
if err != nil {
suite.T().Logf("expected no error, got %v", err)
return false
}
if lookupAcct == nil {
suite.T().Log("expected not nil account lookup")
return false
}
return true
},
},
{
createOrDelete: "delete",
permissionType: gtsmodel.DomainPermissionAllow,
domain: domain,
expected: func(ctx context.Context, account *gtsmodel.Account) bool {
// Deleting the allow now, while there's
// a block in place, should cause the
// block to take effect.
if account.SuspendedAt.IsZero() {
suite.T().Logf("account %s should be suspended", account.Username)
return false
}
// Lookup for this account should return 404.
lookupAcct, err := suite.lookupAccount(ctx, testAccount, account)
if err == nil || err.Code() != http.StatusNotFound {
suite.T().Logf("expected 404 error, got %v", err)
return false
}
if lookupAcct != nil {
suite.T().Logf("expected nil account lookup, got %v", lookupAcct)
return false
}
return true
}, },
}, },
}, },