mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 17:42:24 -05:00 
			
		
		
		
	[feature] Implement explicit domain allows + allowlist federation mode (#2200)
* love like winter! wohoah, wohoah * domain allow side effects * tests! logging! unallow! * document federation modes * linty linterson * test * further adventures in documentation * finish up domain block documentation (i think) * change wording a wee little bit * docs, example * consolidate shared domainPermission code * call mode once * fetch federation mode within domain blocked func * read domain perm import in streaming manner * don't use pointer to slice for domain perms * don't bother copying blocks + allows before deleting * admonish! * change wording just a scooch * update docs
This commit is contained in:
		
					parent
					
						
							
								d6add4ef93
							
						
					
				
			
			
				commit
				
					
						183eaa5b29
					
				
			
		
					 52 changed files with 2877 additions and 730 deletions
				
			
		
							
								
								
									
										335
									
								
								internal/processing/admin/domainpermission.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								internal/processing/admin/domainpermission.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue