diff options
Diffstat (limited to 'internal/processing/admin')
| -rw-r--r-- | internal/processing/admin/domainallow.go | 255 | ||||
| -rw-r--r-- | internal/processing/admin/domainblock.go | 305 | ||||
| -rw-r--r-- | internal/processing/admin/domainblock_test.go | 76 | ||||
| -rw-r--r-- | internal/processing/admin/domainpermission.go | 335 | ||||
| -rw-r--r-- | internal/processing/admin/domainpermission_test.go | 280 | ||||
| -rw-r--r-- | internal/processing/admin/util.go | 17 | 
6 files changed, 944 insertions, 324 deletions
| diff --git a/internal/processing/admin/domainallow.go b/internal/processing/admin/domainallow.go new file mode 100644 index 000000000..bab54e308 --- /dev/null +++ b/internal/processing/admin/domainallow.go @@ -0,0 +1,255 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// 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 admin + +import ( +	"context" +	"errors" +	"fmt" + +	"codeberg.org/gruf/go-kv" +	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/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/log" +	"github.com/superseriousbusiness/gotosocial/internal/text" +) + +func (p *Processor) createDomainAllow( +	ctx context.Context, +	adminAcct *gtsmodel.Account, +	domain string, +	obfuscate bool, +	publicComment string, +	privateComment string, +	subscriptionID string, +) (*apimodel.DomainPermission, string, gtserror.WithCode) { +	// Check if an allow already exists for this domain. +	domainAllow, err := p.state.DB.GetDomainAllow(ctx, domain) +	if err != nil && !errors.Is(err, db.ErrNoEntries) { +		// Something went wrong in the DB. +		err = gtserror.Newf("db error getting domain allow %s: %w", domain, err) +		return nil, "", gtserror.NewErrorInternalError(err) +	} + +	if domainAllow == nil { +		// No allow exists yet, create it. +		domainAllow = >smodel.DomainAllow{ +			ID:                 id.NewULID(), +			Domain:             domain, +			CreatedByAccountID: adminAcct.ID, +			PrivateComment:     text.SanitizeToPlaintext(privateComment), +			PublicComment:      text.SanitizeToPlaintext(publicComment), +			Obfuscate:          &obfuscate, +			SubscriptionID:     subscriptionID, +		} + +		// Insert the new allow into the database. +		if err := p.state.DB.CreateDomainAllow(ctx, domainAllow); err != nil { +			err = gtserror.Newf("db error putting domain allow %s: %w", domain, err) +			return nil, "", gtserror.NewErrorInternalError(err) +		} +	} + +	actionID := id.NewULID() + +	// Process domain allow side +	// effects asynchronously. +	if errWithCode := p.actions.Run( +		ctx, +		>smodel.AdminAction{ +			ID:             actionID, +			TargetCategory: gtsmodel.AdminActionCategoryDomain, +			TargetID:       domain, +			Type:           gtsmodel.AdminActionSuspend, +			AccountID:      adminAcct.ID, +			Text:           domainAllow.PrivateComment, +		}, +		func(ctx context.Context) gtserror.MultiError { +			// Log start + finish. +			l := log.WithFields(kv.Fields{ +				{"domain", domain}, +				{"actionID", actionID}, +			}...).WithContext(ctx) + +			l.Info("processing domain allow side effects") +			defer func() { l.Info("finished processing domain allow side effects") }() + +			return p.domainAllowSideEffects(ctx, domainAllow) +		}, +	); errWithCode != nil { +		return nil, actionID, errWithCode +	} + +	apiDomainAllow, errWithCode := p.apiDomainPerm(ctx, domainAllow, false) +	if errWithCode != nil { +		return nil, actionID, errWithCode +	} + +	return apiDomainAllow, actionID, nil +} + +func (p *Processor) domainAllowSideEffects( +	ctx context.Context, +	allow *gtsmodel.DomainAllow, +) gtserror.MultiError { +	if config.GetInstanceFederationMode() == config.InstanceFederationModeAllowlist { +		// We're running in allowlist mode, +		// so there are no side effects to +		// process here. +		return nil +	} + +	// We're running in blocklist mode or +	// some similar mode which necessitates +	// domain allow side effects if a block +	// was in place when the allow was created. +	// +	// So, check if there's a block. +	block, err := p.state.DB.GetDomainBlock(ctx, allow.Domain) +	if err != nil && !errors.Is(err, db.ErrNoEntries) { +		errs := gtserror.NewMultiError(1) +		errs.Appendf("db error getting domain block %s: %w", allow.Domain, err) +		return errs +	} + +	if block == nil { +		// No block? +		// No problem! +		return nil +	} + +	// There was a block, over which the new +	// allow ought to take precedence. To account +	// for this, just run side effects as though +	// the domain was being unblocked, while +	// leaving the existing block in place. +	// +	// Any accounts that were suspended by +	// the block will be unsuspended and be +	// able to interact with the instance again. +	return p.domainUnblockSideEffects(ctx, block) +} + +func (p *Processor) deleteDomainAllow( +	ctx context.Context, +	adminAcct *gtsmodel.Account, +	domainAllowID string, +) (*apimodel.DomainPermission, string, gtserror.WithCode) { +	domainAllow, err := p.state.DB.GetDomainAllowByID(ctx, domainAllowID) +	if err != nil { +		if !errors.Is(err, db.ErrNoEntries) { +			// Real error. +			err = gtserror.Newf("db error getting domain allow: %w", err) +			return nil, "", gtserror.NewErrorInternalError(err) +		} + +		// There are just no entries for this ID. +		err = fmt.Errorf("no domain allow entry exists with ID %s", domainAllowID) +		return nil, "", gtserror.NewErrorNotFound(err, err.Error()) +	} + +	// Prepare the domain allow to return, *before* the deletion goes through. +	apiDomainAllow, errWithCode := p.apiDomainPerm(ctx, domainAllow, false) +	if errWithCode != nil { +		return nil, "", errWithCode +	} + +	// Delete the original domain allow. +	if err := p.state.DB.DeleteDomainAllow(ctx, domainAllow.Domain); err != nil { +		err = gtserror.Newf("db error deleting domain allow: %w", err) +		return nil, "", gtserror.NewErrorInternalError(err) +	} + +	actionID := id.NewULID() + +	// Process domain unallow side +	// effects asynchronously. +	if errWithCode := p.actions.Run( +		ctx, +		>smodel.AdminAction{ +			ID:             actionID, +			TargetCategory: gtsmodel.AdminActionCategoryDomain, +			TargetID:       domainAllow.Domain, +			Type:           gtsmodel.AdminActionUnsuspend, +			AccountID:      adminAcct.ID, +		}, +		func(ctx context.Context) gtserror.MultiError { +			// Log start + finish. +			l := log.WithFields(kv.Fields{ +				{"domain", domainAllow.Domain}, +				{"actionID", actionID}, +			}...).WithContext(ctx) + +			l.Info("processing domain unallow side effects") +			defer func() { l.Info("finished processing domain unallow side effects") }() + +			return p.domainUnallowSideEffects(ctx, domainAllow) +		}, +	); errWithCode != nil { +		return nil, actionID, errWithCode +	} + +	return apiDomainAllow, actionID, nil +} + +func (p *Processor) domainUnallowSideEffects( +	ctx context.Context, +	allow *gtsmodel.DomainAllow, +) gtserror.MultiError { +	if config.GetInstanceFederationMode() == config.InstanceFederationModeAllowlist { +		// We're running in allowlist mode, +		// so there are no side effects to +		// process here. +		return nil +	} + +	// We're running in blocklist mode or +	// some similar mode which necessitates +	// domain allow side effects if a block +	// was in place when the allow was removed. +	// +	// So, check if there's a block. +	block, err := p.state.DB.GetDomainBlock(ctx, allow.Domain) +	if err != nil && !errors.Is(err, db.ErrNoEntries) { +		errs := gtserror.NewMultiError(1) +		errs.Appendf("db error getting domain block %s: %w", allow.Domain, err) +		return errs +	} + +	if block == nil { +		// No block? +		// No problem! +		return nil +	} + +	// There was a block, over which the previous +	// allow was taking precedence. Now that the +	// allow has been removed, we should put the +	// side effects of the block back in place. +	// +	// To do this, process the block side effects +	// again as though the block were freshly +	// created. This will mark all accounts from +	// the blocked domain as suspended, and clean +	// up their follows/following, media, etc. +	return p.domainBlockSideEffects(ctx, block) +} diff --git a/internal/processing/admin/domainblock.go b/internal/processing/admin/domainblock.go index 1262bf6b0..4161ec12f 100644 --- a/internal/processing/admin/domainblock.go +++ b/internal/processing/admin/domainblock.go @@ -18,14 +18,9 @@  package admin  import ( -	"bytes"  	"context" -	"encoding/json"  	"errors"  	"fmt" -	"io" -	"mime/multipart" -	"net/http"  	"time"  	"codeberg.org/gruf/go-kv" @@ -40,14 +35,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/text"  ) -// DomainBlockCreate creates an instance-level block against the given domain, -// and then processes side effects of that block (deleting accounts, media, etc). -// -// If a domain block already exists for the domain, side effects will be retried. -// -// Return values for this function are the (new) domain block, the ID of the admin -// action resulting from this call, and/or an error if something goes wrong. -func (p *Processor) DomainBlockCreate( +func (p *Processor) createDomainBlock(  	ctx context.Context,  	adminAcct *gtsmodel.Account,  	domain string, @@ -55,7 +43,7 @@ func (p *Processor) DomainBlockCreate(  	publicComment string,  	privateComment string,  	subscriptionID string, -) (*apimodel.DomainBlock, string, gtserror.WithCode) { +) (*apimodel.DomainPermission, string, gtserror.WithCode) {  	// Check if a block already exists for this domain.  	domainBlock, err := p.state.DB.GetDomainBlock(ctx, domain)  	if err != nil && !errors.Is(err, db.ErrNoEntries) { @@ -98,13 +86,22 @@ func (p *Processor) DomainBlockCreate(  			Text:           domainBlock.PrivateComment,  		},  		func(ctx context.Context) gtserror.MultiError { +			// Log start + finish. +			l := log.WithFields(kv.Fields{ +				{"domain", domain}, +				{"actionID", actionID}, +			}...).WithContext(ctx) + +			l.Info("processing domain block side effects") +			defer func() { l.Info("finished processing domain block side effects") }() +  			return p.domainBlockSideEffects(ctx, domainBlock)  		},  	); errWithCode != nil {  		return nil, actionID, errWithCode  	} -	apiDomainBlock, errWithCode := p.apiDomainBlock(ctx, domainBlock) +	apiDomainBlock, errWithCode := p.apiDomainPerm(ctx, domainBlock, false)  	if errWithCode != nil {  		return nil, actionID, errWithCode  	} @@ -112,206 +109,6 @@ func (p *Processor) DomainBlockCreate(  	return apiDomainBlock, actionID, nil  } -// DomainBlockDelete removes one domain block with the given ID, -// and processes side effects of removing the block asynchronously. -// -// Return values for this function are the deleted domain block, the ID of the admin -// action resulting from this call, and/or an error if something goes wrong. -func (p *Processor) DomainBlockDelete( -	ctx context.Context, -	adminAcct *gtsmodel.Account, -	domainBlockID string, -) (*apimodel.DomainBlock, string, gtserror.WithCode) { -	domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, domainBlockID) -	if err != nil { -		if !errors.Is(err, db.ErrNoEntries) { -			// Real error. -			err = gtserror.Newf("db error getting domain block: %w", err) -			return nil, "", gtserror.NewErrorInternalError(err) -		} - -		// There are just no entries for this ID. -		err = fmt.Errorf("no domain block entry exists with ID %s", domainBlockID) -		return nil, "", gtserror.NewErrorNotFound(err, err.Error()) -	} - -	// Prepare the domain block to return, *before* the deletion goes through. -	apiDomainBlock, errWithCode := p.apiDomainBlock(ctx, domainBlock) -	if errWithCode != nil { -		return nil, "", errWithCode -	} - -	// Copy value of the domain block. -	domainBlockC := new(gtsmodel.DomainBlock) -	*domainBlockC = *domainBlock - -	// Delete the original domain block. -	if err := p.state.DB.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil { -		err = gtserror.Newf("db error deleting domain block: %w", err) -		return nil, "", gtserror.NewErrorInternalError(err) -	} - -	actionID := id.NewULID() - -	// Process domain unblock side -	// effects asynchronously. -	if errWithCode := p.actions.Run( -		ctx, -		>smodel.AdminAction{ -			ID:             actionID, -			TargetCategory: gtsmodel.AdminActionCategoryDomain, -			TargetID:       domainBlockC.Domain, -			Type:           gtsmodel.AdminActionUnsuspend, -			AccountID:      adminAcct.ID, -		}, -		func(ctx context.Context) gtserror.MultiError { -			return p.domainUnblockSideEffects(ctx, domainBlock) -		}, -	); errWithCode != nil { -		return nil, actionID, errWithCode -	} - -	return apiDomainBlock, actionID, nil -} - -// DomainBlocksImport handles the import of multiple domain blocks, -// by calling the DomainBlockCreate function for each domain in the -// provided file. Will return a slice of processed domain blocks. -// -// In the case of total failure, a gtserror.WithCode will be returned -// so that the caller can respond appropriately. In the case of -// partial or total success, a MultiStatus model will be returned, -// which contains information about success/failure count, so that -// the caller can retry any failures as they wish. -func (p *Processor) DomainBlocksImport( -	ctx context.Context, -	account *gtsmodel.Account, -	domainsF *multipart.FileHeader, -) (*apimodel.MultiStatus, gtserror.WithCode) { -	// Open the provided file. -	file, err := domainsF.Open() -	if err != nil { -		err = gtserror.Newf("error opening attachment: %w", err) -		return nil, gtserror.NewErrorBadRequest(err, err.Error()) -	} -	defer file.Close() - -	// Copy the file contents into a buffer. -	buf := new(bytes.Buffer) -	size, err := io.Copy(buf, file) -	if err != nil { -		err = gtserror.Newf("error reading attachment: %w", err) -		return nil, gtserror.NewErrorBadRequest(err, err.Error()) -	} - -	// Ensure we actually read something. -	if size == 0 { -		err = gtserror.New("error reading attachment: size 0 bytes") -		return nil, gtserror.NewErrorBadRequest(err, err.Error()) -	} - -	// Parse bytes as slice of domain blocks. -	domainBlocks := make([]*apimodel.DomainBlock, 0) -	if err := json.Unmarshal(buf.Bytes(), &domainBlocks); err != nil { -		err = gtserror.Newf("error parsing attachment as domain blocks: %w", err) -		return nil, gtserror.NewErrorBadRequest(err, err.Error()) -	} - -	count := len(domainBlocks) -	if count == 0 { -		err = gtserror.New("error importing domain blocks: 0 entries provided") -		return nil, gtserror.NewErrorBadRequest(err, err.Error()) -	} - -	// Try to process each domain block, differentiating -	// between successes and errors so that the caller can -	// try failed imports again if desired. -	multiStatusEntries := make([]apimodel.MultiStatusEntry, 0, count) - -	for _, domainBlock := range domainBlocks { -		var ( -			domain         = domainBlock.Domain.Domain -			obfuscate      = domainBlock.Obfuscate -			publicComment  = domainBlock.PublicComment -			privateComment = domainBlock.PrivateComment -			subscriptionID = "" // No sub ID for imports. -			errWithCode    gtserror.WithCode -		) - -		domainBlock, _, errWithCode = p.DomainBlockCreate( -			ctx, -			account, -			domain, -			obfuscate, -			publicComment, -			privateComment, -			subscriptionID, -		) - -		var entry *apimodel.MultiStatusEntry - -		if errWithCode != nil { -			entry = &apimodel.MultiStatusEntry{ -				// Use the failed domain entry as the resource value. -				Resource: domain, -				Message:  errWithCode.Safe(), -				Status:   errWithCode.Code(), -			} -		} else { -			entry = &apimodel.MultiStatusEntry{ -				// Use successfully created API model domain block as the resource value. -				Resource: domainBlock, -				Message:  http.StatusText(http.StatusOK), -				Status:   http.StatusOK, -			} -		} - -		multiStatusEntries = append(multiStatusEntries, *entry) -	} - -	return apimodel.NewMultiStatus(multiStatusEntries), nil -} - -// DomainBlocksGet returns all existing domain blocks. If export is -// true, the format will be suitable for writing out to an export. -func (p *Processor) DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) { -	domainBlocks, err := p.state.DB.GetDomainBlocks(ctx) -	if err != nil && !errors.Is(err, db.ErrNoEntries) { -		err = gtserror.Newf("db error getting domain blocks: %w", err) -		return nil, gtserror.NewErrorInternalError(err) -	} - -	apiDomainBlocks := make([]*apimodel.DomainBlock, 0, len(domainBlocks)) -	for _, domainBlock := range domainBlocks { -		apiDomainBlock, errWithCode := p.apiDomainBlock(ctx, domainBlock) -		if errWithCode != nil { -			return nil, errWithCode -		} - -		apiDomainBlocks = append(apiDomainBlocks, apiDomainBlock) -	} - -	return apiDomainBlocks, nil -} - -// DomainBlockGet returns one domain block with the given id. If export -// is true, the format will be suitable for writing out to an export. -func (p *Processor) DomainBlockGet(ctx context.Context, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) { -	domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, id) -	if err != nil { -		if errors.Is(err, db.ErrNoEntries) { -			err = fmt.Errorf("no domain block exists with id %s", id) -			return nil, gtserror.NewErrorNotFound(err, err.Error()) -		} - -		// Something went wrong in the DB. -		err = gtserror.Newf("db error getting domain block %s: %w", id, err) -		return nil, gtserror.NewErrorInternalError(err) -	} - -	return p.apiDomainBlock(ctx, domainBlock) -} -  // domainBlockSideEffects processes the side effects of a domain block:  //  //  1. Strip most info away from the instance entry for the domain. @@ -323,13 +120,6 @@ func (p *Processor) domainBlockSideEffects(  	ctx context.Context,  	block *gtsmodel.DomainBlock,  ) gtserror.MultiError { -	l := log. -		WithContext(ctx). -		WithFields(kv.Fields{ -			{"domain", block.Domain}, -		}...) -	l.Debug("processing domain block side effects") -  	var errs gtserror.MultiError  	// If we have an instance entry for this domain, @@ -347,7 +137,6 @@ func (p *Processor) domainBlockSideEffects(  			errs.Appendf("db error updating instance: %w", err)  			return errs  		} -		l.Debug("instance entry updated")  	}  	// For each account that belongs to this domain, @@ -372,6 +161,68 @@ func (p *Processor) domainBlockSideEffects(  	return errs  } +func (p *Processor) deleteDomainBlock( +	ctx context.Context, +	adminAcct *gtsmodel.Account, +	domainBlockID string, +) (*apimodel.DomainPermission, string, gtserror.WithCode) { +	domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, domainBlockID) +	if err != nil { +		if !errors.Is(err, db.ErrNoEntries) { +			// Real error. +			err = gtserror.Newf("db error getting domain block: %w", err) +			return nil, "", gtserror.NewErrorInternalError(err) +		} + +		// There are just no entries for this ID. +		err = fmt.Errorf("no domain block entry exists with ID %s", domainBlockID) +		return nil, "", gtserror.NewErrorNotFound(err, err.Error()) +	} + +	// Prepare the domain block to return, *before* the deletion goes through. +	apiDomainBlock, errWithCode := p.apiDomainPerm(ctx, domainBlock, false) +	if errWithCode != nil { +		return nil, "", errWithCode +	} + +	// Delete the original domain block. +	if err := p.state.DB.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil { +		err = gtserror.Newf("db error deleting domain block: %w", err) +		return nil, "", gtserror.NewErrorInternalError(err) +	} + +	actionID := id.NewULID() + +	// Process domain unblock side +	// effects asynchronously. +	if errWithCode := p.actions.Run( +		ctx, +		>smodel.AdminAction{ +			ID:             actionID, +			TargetCategory: gtsmodel.AdminActionCategoryDomain, +			TargetID:       domainBlock.Domain, +			Type:           gtsmodel.AdminActionUnsuspend, +			AccountID:      adminAcct.ID, +		}, +		func(ctx context.Context) gtserror.MultiError { +			// Log start + finish. +			l := log.WithFields(kv.Fields{ +				{"domain", domainBlock.Domain}, +				{"actionID", actionID}, +			}...).WithContext(ctx) + +			l.Info("processing domain unblock side effects") +			defer func() { l.Info("finished processing domain unblock side effects") }() + +			return p.domainUnblockSideEffects(ctx, domainBlock) +		}, +	); errWithCode != nil { +		return nil, actionID, errWithCode +	} + +	return apiDomainBlock, actionID, nil +} +  // domainUnblockSideEffects processes the side effects of undoing a  // domain block:  // @@ -385,13 +236,6 @@ func (p *Processor) domainUnblockSideEffects(  	ctx context.Context,  	block *gtsmodel.DomainBlock,  ) gtserror.MultiError { -	l := log. -		WithContext(ctx). -		WithFields(kv.Fields{ -			{"domain", block.Domain}, -		}...) -	l.Debug("processing domain unblock side effects") -  	var errs gtserror.MultiError  	// Update instance entry for this domain, if we have it. @@ -414,7 +258,6 @@ func (p *Processor) domainUnblockSideEffects(  			errs.Appendf("db error updating instance: %w", err)  			return errs  		} -		l.Debug("instance entry updated")  	}  	// Unsuspend all accounts whose suspension origin was this domain block. diff --git a/internal/processing/admin/domainblock_test.go b/internal/processing/admin/domainblock_test.go deleted file mode 100644 index 9525ce7c3..000000000 --- a/internal/processing/admin/domainblock_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// 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 admin_test - -import ( -	"context" -	"testing" - -	"github.com/stretchr/testify/suite" -	"github.com/superseriousbusiness/gotosocial/testrig" -) - -type DomainBlockTestSuite struct { -	AdminStandardTestSuite -} - -func (suite *DomainBlockTestSuite) TestCreateDomainBlock() { -	var ( -		ctx            = context.Background() -		adminAcct      = suite.testAccounts["admin_account"] -		domain         = "fossbros-anonymous.io" -		obfuscate      = false -		publicComment  = "" -		privateComment = "" -		subscriptionID = "" -	) - -	apiBlock, actionID, errWithCode := suite.adminProcessor.DomainBlockCreate( -		ctx, -		adminAcct, -		domain, -		obfuscate, -		publicComment, -		privateComment, -		subscriptionID, -	) -	suite.NoError(errWithCode) -	suite.NotNil(apiBlock) -	suite.NotEmpty(actionID) - -	// Wait for action to finish. -	if !testrig.WaitFor(func() bool { -		return suite.adminProcessor.Actions().TotalRunning() == 0 -	}) { -		suite.FailNow("timed out waiting for admin action(s) to finish") -	} - -	// Ensure action marked as -	// completed in the database. -	adminAction, err := suite.db.GetAdminAction(ctx, actionID) -	if err != nil { -		suite.FailNow(err.Error()) -	} - -	suite.NotZero(adminAction.CompletedAt) -	suite.Empty(adminAction.Errors) -} - -func TestDomainBlockTestSuite(t *testing.T) { -	suite.Run(t, new(DomainBlockTestSuite)) -} diff --git a/internal/processing/admin/domainpermission.go b/internal/processing/admin/domainpermission.go new file mode 100644 index 000000000..c759c0f11 --- /dev/null +++ b/internal/processing/admin/domainpermission.go @@ -0,0 +1,335 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// 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 admin + +import ( +	"context" +	"encoding/json" +	"errors" +	"fmt" +	"mime/multipart" +	"net/http" + +	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" +) + +// apiDomainPerm is a cheeky shortcut for returning +// the API version of the given domain permission +// (*gtsmodel.DomainBlock or *gtsmodel.DomainAllow), +// or an appropriate error if something goes wrong. +func (p *Processor) apiDomainPerm( +	ctx context.Context, +	domainPermission gtsmodel.DomainPermission, +	export bool, +) (*apimodel.DomainPermission, gtserror.WithCode) { +	apiDomainPerm, err := p.tc.DomainPermToAPIDomainPerm(ctx, domainPermission, export) +	if err != nil { +		err := gtserror.NewfAt(3, "error converting domain permission to api model: %w", err) +		return nil, gtserror.NewErrorInternalError(err) +	} + +	return apiDomainPerm, nil +} + +// DomainPermissionCreate creates an instance-level permission +// targeting the given domain, and then processes any side +// effects of the permission creation. +// +// If the same permission type already exists for the domain, +// side effects will be retried. +// +// Return values for this function are the new or existing +// domain permission, the ID of the admin action resulting +// from this call, and/or an error if something goes wrong. +func (p *Processor) DomainPermissionCreate( +	ctx context.Context, +	permissionType gtsmodel.DomainPermissionType, +	adminAcct *gtsmodel.Account, +	domain string, +	obfuscate bool, +	publicComment string, +	privateComment string, +	subscriptionID string, +) (*apimodel.DomainPermission, string, gtserror.WithCode) { +	switch permissionType { + +	// Explicitly block a domain. +	case gtsmodel.DomainPermissionBlock: +		return p.createDomainBlock( +			ctx, +			adminAcct, +			domain, +			obfuscate, +			publicComment, +			privateComment, +			subscriptionID, +		) + +	// Explicitly allow a domain. +	case gtsmodel.DomainPermissionAllow: +		return p.createDomainAllow( +			ctx, +			adminAcct, +			domain, +			obfuscate, +			publicComment, +			privateComment, +			subscriptionID, +		) + +	// Weeping, roaring, red-faced. +	default: +		err := gtserror.Newf("unrecognized permission type %d", permissionType) +		return nil, "", gtserror.NewErrorInternalError(err) +	} +} + +// DomainPermissionDelete removes one domain block with the given ID, +// and processes side effects of removing the block asynchronously. +// +// Return values for this function are the deleted domain block, the ID of the admin +// action resulting from this call, and/or an error if something goes wrong. +func (p *Processor) DomainPermissionDelete( +	ctx context.Context, +	permissionType gtsmodel.DomainPermissionType, +	adminAcct *gtsmodel.Account, +	domainBlockID string, +) (*apimodel.DomainPermission, string, gtserror.WithCode) { +	switch permissionType { + +	// Delete explicit domain block. +	case gtsmodel.DomainPermissionBlock: +		return p.deleteDomainBlock( +			ctx, +			adminAcct, +			domainBlockID, +		) + +	// Delete explicit domain allow. +	case gtsmodel.DomainPermissionAllow: +		return p.deleteDomainAllow( +			ctx, +			adminAcct, +			domainBlockID, +		) + +	// You do the hokey-cokey and you turn +	// around, that's what it's all about. +	default: +		err := gtserror.Newf("unrecognized permission type %d", permissionType) +		return nil, "", gtserror.NewErrorInternalError(err) +	} +} + +// DomainPermissionsImport handles the import of multiple +// domain permissions, by calling the DomainPermissionCreate +// function for each domain in the provided file. Will return +// a slice of processed domain permissions. +// +// In the case of total failure, a gtserror.WithCode will be +// returned so that the caller can respond appropriately. In +// the case of partial or total success, a MultiStatus model +// will be returned, which contains information about success +// + failure count, so that the caller can retry any failures +// as they wish. +func (p *Processor) DomainPermissionsImport( +	ctx context.Context, +	permissionType gtsmodel.DomainPermissionType, +	account *gtsmodel.Account, +	domainsF *multipart.FileHeader, +) (*apimodel.MultiStatus, gtserror.WithCode) { +	// Ensure known permission type. +	if permissionType != gtsmodel.DomainPermissionBlock && +		permissionType != gtsmodel.DomainPermissionAllow { +		err := gtserror.Newf("unrecognized permission type %d", permissionType) +		return nil, gtserror.NewErrorInternalError(err) +	} + +	// Open the provided file. +	file, err := domainsF.Open() +	if err != nil { +		err = gtserror.Newf("error opening attachment: %w", err) +		return nil, gtserror.NewErrorBadRequest(err, err.Error()) +	} +	defer file.Close() + +	// Parse file as slice of domain blocks. +	domainPerms := make([]*apimodel.DomainPermission, 0) +	if err := json.NewDecoder(file).Decode(&domainPerms); err != nil { +		err = gtserror.Newf("error parsing attachment as domain permissions: %w", err) +		return nil, gtserror.NewErrorBadRequest(err, err.Error()) +	} + +	count := len(domainPerms) +	if count == 0 { +		err = gtserror.New("error importing domain permissions: 0 entries provided") +		return nil, gtserror.NewErrorBadRequest(err, err.Error()) +	} + +	// Try to process each domain permission, differentiating +	// between successes and errors so that the caller can +	// try failed imports again if desired. +	multiStatusEntries := make([]apimodel.MultiStatusEntry, 0, count) + +	for _, domainPerm := range domainPerms { +		var ( +			domain         = domainPerm.Domain.Domain +			obfuscate      = domainPerm.Obfuscate +			publicComment  = domainPerm.PublicComment +			privateComment = domainPerm.PrivateComment +			subscriptionID = "" // No sub ID for imports. +			errWithCode    gtserror.WithCode +		) + +		domainPerm, _, errWithCode = p.DomainPermissionCreate( +			ctx, +			permissionType, +			account, +			domain, +			obfuscate, +			publicComment, +			privateComment, +			subscriptionID, +		) + +		var entry *apimodel.MultiStatusEntry + +		if errWithCode != nil { +			entry = &apimodel.MultiStatusEntry{ +				// Use the failed domain entry as the resource value. +				Resource: domain, +				Message:  errWithCode.Safe(), +				Status:   errWithCode.Code(), +			} +		} else { +			entry = &apimodel.MultiStatusEntry{ +				// Use successfully created API model domain block as the resource value. +				Resource: domainPerm, +				Message:  http.StatusText(http.StatusOK), +				Status:   http.StatusOK, +			} +		} + +		multiStatusEntries = append(multiStatusEntries, *entry) +	} + +	return apimodel.NewMultiStatus(multiStatusEntries), nil +} + +// DomainPermissionsGet returns all existing domain +// permissions of the requested type. If export is +// true, the format will be suitable for writing out +// to an export. +func (p *Processor) DomainPermissionsGet( +	ctx context.Context, +	permissionType gtsmodel.DomainPermissionType, +	account *gtsmodel.Account, +	export bool, +) ([]*apimodel.DomainPermission, gtserror.WithCode) { +	var ( +		domainPerms []gtsmodel.DomainPermission +		err         error +	) + +	switch permissionType { +	case gtsmodel.DomainPermissionBlock: +		var blocks []*gtsmodel.DomainBlock + +		blocks, err = p.state.DB.GetDomainBlocks(ctx) +		if err != nil { +			break +		} + +		for _, block := range blocks { +			domainPerms = append(domainPerms, block) +		} + +	case gtsmodel.DomainPermissionAllow: +		var allows []*gtsmodel.DomainAllow + +		allows, err = p.state.DB.GetDomainAllows(ctx) +		if err != nil { +			break +		} + +		for _, allow := range allows { +			domainPerms = append(domainPerms, allow) +		} + +	default: +		err = errors.New("unrecognized permission type") +	} + +	if err != nil { +		err := gtserror.Newf("error getting %ss: %w", permissionType.String(), err) +		return nil, gtserror.NewErrorInternalError(err) +	} + +	apiDomainPerms := make([]*apimodel.DomainPermission, len(domainPerms)) +	for i, domainPerm := range domainPerms { +		apiDomainBlock, errWithCode := p.apiDomainPerm(ctx, domainPerm, export) +		if errWithCode != nil { +			return nil, errWithCode +		} + +		apiDomainPerms[i] = apiDomainBlock +	} + +	return apiDomainPerms, nil +} + +// DomainPermissionGet returns one domain +// permission with the given id and type. +// +// If export is true, the format will be +// suitable for writing out to an export. +func (p *Processor) DomainPermissionGet( +	ctx context.Context, +	permissionType gtsmodel.DomainPermissionType, +	id string, +	export bool, +) (*apimodel.DomainPermission, gtserror.WithCode) { +	var ( +		domainPerm gtsmodel.DomainPermission +		err        error +	) + +	switch permissionType { +	case gtsmodel.DomainPermissionBlock: +		domainPerm, err = p.state.DB.GetDomainBlockByID(ctx, id) +	case gtsmodel.DomainPermissionAllow: +		domainPerm, err = p.state.DB.GetDomainAllowByID(ctx, id) +	default: +		err = gtserror.New("unrecognized permission type") +	} + +	if err != nil { +		if errors.Is(err, db.ErrNoEntries) { +			err = fmt.Errorf("no domain %s exists with id %s", permissionType.String(), id) +			return nil, gtserror.NewErrorNotFound(err, err.Error()) +		} + +		err = gtserror.Newf("error getting domain %s with id %s: %w", permissionType.String(), id, err) +		return nil, gtserror.NewErrorInternalError(err) +	} + +	return p.apiDomainPerm(ctx, domainPerm, export) +} diff --git a/internal/processing/admin/domainpermission_test.go b/internal/processing/admin/domainpermission_test.go new file mode 100644 index 000000000..b6de226c1 --- /dev/null +++ b/internal/processing/admin/domainpermission_test.go @@ -0,0 +1,280 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// 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 admin_test + +import ( +	"context" +	"testing" + +	"github.com/stretchr/testify/suite" +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/testrig" +) + +type DomainBlockTestSuite struct { +	AdminStandardTestSuite +} + +type domainPermAction struct { +	// 'create' or 'delete' +	// the domain permission. +	createOrDelete string + +	// Type of permission +	// to create or delete. +	permissionType gtsmodel.DomainPermissionType + +	// Domain to target +	// with the permission. +	domain string + +	// Expected result of this +	// permission action on each +	// account on the target domain. +	// Eg., suite.Zero(account.SuspendedAt) +	expected func(*gtsmodel.Account) bool +} + +type domainPermTest struct { +	// Federation mode under which to +	// run this test. This is important +	// because it may effect which side +	// effects are taken, if any. +	instanceFederationMode string + +	// Series of actions to run as part +	// of this test. After each action, +	// expected will be called. This +	// allows testers to run multiple +	// actions in a row and check that +	// the results after each action are +	// what they expected, in light of +	// previous actions. +	actions []domainPermAction +} + +// run a domainPermTest by running each of +// its actions in turn and checking results. +func (suite *DomainBlockTestSuite) runDomainPermTest(t domainPermTest) { +	config.SetInstanceFederationMode(t.instanceFederationMode) + +	for _, action := range t.actions { +		// Run the desired action. +		var actionID string +		switch action.createOrDelete { +		case "create": +			_, actionID = suite.createDomainPerm(action.permissionType, action.domain) +		case "delete": +			_, actionID = suite.deleteDomainPerm(action.permissionType, action.domain) +		default: +			panic("createOrDelete was not 'create' or 'delete'") +		} + +		// Let the action finish. +		suite.awaitAction(actionID) + +		// Check expected results +		// against each account. +		accounts, err := suite.db.GetInstanceAccounts( +			context.Background(), +			action.domain, +			"", 0, +		) +		if err != nil { +			suite.FailNow("", "error getting instance accounts for %s: %v", action.domain, err) +		} + +		for _, account := range accounts { +			if !action.expected(account) { +				suite.T().FailNow() +			} +		} +	} +} + +// create given permissionType with default values. +func (suite *DomainBlockTestSuite) createDomainPerm( +	permissionType gtsmodel.DomainPermissionType, +	domain string, +) (*apimodel.DomainPermission, string) { +	ctx := context.Background() + +	apiPerm, actionID, errWithCode := suite.adminProcessor.DomainPermissionCreate( +		ctx, +		permissionType, +		suite.testAccounts["admin_account"], +		domain, +		false, +		"", +		"", +		"", +	) +	suite.NoError(errWithCode) +	suite.NotNil(apiPerm) +	suite.NotEmpty(actionID) + +	return apiPerm, actionID +} + +// delete given permission type. +func (suite *DomainBlockTestSuite) deleteDomainPerm( +	permissionType gtsmodel.DomainPermissionType, +	domain string, +) (*apimodel.DomainPermission, string) { +	var ( +		ctx              = context.Background() +		domainPermission gtsmodel.DomainPermission +	) + +	// To delete the permission, +	// first get it from the db. +	switch permissionType { +	case gtsmodel.DomainPermissionBlock: +		domainPermission, _ = suite.db.GetDomainBlock(ctx, domain) +	case gtsmodel.DomainPermissionAllow: +		domainPermission, _ = suite.db.GetDomainAllow(ctx, domain) +	default: +		panic("unrecognized permission type") +	} + +	if domainPermission == nil { +		suite.FailNow("domain permission was nil") +	} + +	// Now use the ID to delete it. +	apiPerm, actionID, errWithCode := suite.adminProcessor.DomainPermissionDelete( +		ctx, +		permissionType, +		suite.testAccounts["admin_account"], +		domainPermission.GetID(), +	) +	suite.NoError(errWithCode) +	suite.NotNil(apiPerm) +	suite.NotEmpty(actionID) + +	return apiPerm, actionID +} + +// waits for given actionID to be completed. +func (suite *DomainBlockTestSuite) awaitAction(actionID string) { +	ctx := context.Background() + +	if !testrig.WaitFor(func() bool { +		return suite.adminProcessor.Actions().TotalRunning() == 0 +	}) { +		suite.FailNow("timed out waiting for admin action(s) to finish") +	} + +	// Ensure action marked as +	// completed in the database. +	adminAction, err := suite.db.GetAdminAction(ctx, actionID) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.NotZero(adminAction.CompletedAt) +	suite.Empty(adminAction.Errors) +} + +func (suite *DomainBlockTestSuite) TestBlockAndUnblockDomain() { +	const domain = "fossbros-anonymous.io" + +	suite.runDomainPermTest(domainPermTest{ +		instanceFederationMode: config.InstanceFederationModeBlocklist, +		actions: []domainPermAction{ +			{ +				createOrDelete: "create", +				permissionType: gtsmodel.DomainPermissionBlock, +				domain:         domain, +				expected: func(account *gtsmodel.Account) bool { +					// Domain was blocked, so each +					// account should now be suspended. +					return suite.NotZero(account.SuspendedAt) +				}, +			}, +			{ +				createOrDelete: "delete", +				permissionType: gtsmodel.DomainPermissionBlock, +				domain:         domain, +				expected: func(account *gtsmodel.Account) bool { +					// Domain was unblocked, so each +					// account should now be unsuspended. +					return suite.Zero(account.SuspendedAt) +				}, +			}, +		}, +	}) +} + +func (suite *DomainBlockTestSuite) TestBlockAndAllowDomain() { +	const domain = "fossbros-anonymous.io" + +	suite.runDomainPermTest(domainPermTest{ +		instanceFederationMode: config.InstanceFederationModeBlocklist, +		actions: []domainPermAction{ +			{ +				createOrDelete: "create", +				permissionType: gtsmodel.DomainPermissionBlock, +				domain:         domain, +				expected: func(account *gtsmodel.Account) bool { +					// Domain was blocked, so each +					// account should now be suspended. +					return suite.NotZero(account.SuspendedAt) +				}, +			}, +			{ +				createOrDelete: "create", +				permissionType: gtsmodel.DomainPermissionAllow, +				domain:         domain, +				expected: func(account *gtsmodel.Account) bool { +					// Domain was explicitly allowed, so each +					// account should now be unsuspended, since +					// the allow supercedes the block. +					return suite.Zero(account.SuspendedAt) +				}, +			}, +			{ +				createOrDelete: "delete", +				permissionType: gtsmodel.DomainPermissionAllow, +				domain:         domain, +				expected: func(account *gtsmodel.Account) bool { +					// Deleting the allow now, while there's +					// still a block in place, should cause +					// the block to take effect again. +					return suite.NotZero(account.SuspendedAt) +				}, +			}, +			{ +				createOrDelete: "delete", +				permissionType: gtsmodel.DomainPermissionBlock, +				domain:         domain, +				expected: func(account *gtsmodel.Account) bool { +					// Deleting the block now should +					// unsuspend the accounts again. +					return suite.Zero(account.SuspendedAt) +				}, +			}, +		}, +	}) +} + +func TestDomainBlockTestSuite(t *testing.T) { +	suite.Run(t, new(DomainBlockTestSuite)) +} diff --git a/internal/processing/admin/util.go b/internal/processing/admin/util.go index 403602901..c82ff2dc1 100644 --- a/internal/processing/admin/util.go +++ b/internal/processing/admin/util.go @@ -22,28 +22,11 @@ import (  	"errors"  	"time" -	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"  ) -// apiDomainBlock is a cheeky shortcut for returning -// the API version of the given domainBlock, or an -// appropriate error if something goes wrong. -func (p *Processor) apiDomainBlock( -	ctx context.Context, -	domainBlock *gtsmodel.DomainBlock, -) (*apimodel.DomainBlock, gtserror.WithCode) { -	apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, false) -	if err != nil { -		err = gtserror.Newf("error converting domain block for %s to api model : %w", domainBlock.Domain, err) -		return nil, gtserror.NewErrorInternalError(err) -	} - -	return apiDomainBlock, nil -} -  // stubbifyInstance renders the given instance as a stub,  // removing most information from it and marking it as  // suspended. | 
