summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/api/client/admin/admin.go8
-rw-r--r--internal/api/client/admin/domainallowcreate.go128
-rw-r--r--internal/api/client/admin/domainallowdelete.go72
-rw-r--r--internal/api/client/admin/domainallowget.go67
-rw-r--r--internal/api/client/admin/domainallowsget.go73
-rw-r--r--internal/api/client/admin/domainblockcreate.go118
-rw-r--r--internal/api/client/admin/domainblockdelete.go42
-rw-r--r--internal/api/client/admin/domainblockget.go46
-rw-r--r--internal/api/client/admin/domainblocksget.go40
-rw-r--r--internal/api/client/admin/domainpermission.go295
-rw-r--r--internal/api/model/domain.go43
-rw-r--r--internal/api/util/parsequery.go14
-rw-r--r--internal/cache/domain/domain.go51
-rw-r--r--internal/cache/domain/domain_test.go30
-rw-r--r--internal/cache/gts.go17
-rw-r--r--internal/config/config.go13
-rw-r--r--internal/config/const.go26
-rw-r--r--internal/config/defaults.go1
-rw-r--r--internal/config/flags.go1
-rw-r--r--internal/config/helpers.gen.go25
-rw-r--r--internal/config/validate.go11
-rw-r--r--internal/db/bundb/domain.go148
-rw-r--r--internal/db/bundb/domain_test.go53
-rw-r--r--internal/db/bundb/migrations/20230908083121_allowlist.go.go62
-rw-r--r--internal/db/domain.go34
-rw-r--r--internal/gtsmodel/adminaction.go2
-rw-r--r--internal/gtsmodel/domainallow.go78
-rw-r--r--internal/gtsmodel/domainblock.go44
-rw-r--r--internal/gtsmodel/domainpermission.go67
-rw-r--r--internal/processing/admin/domainallow.go255
-rw-r--r--internal/processing/admin/domainblock.go305
-rw-r--r--internal/processing/admin/domainblock_test.go76
-rw-r--r--internal/processing/admin/domainpermission.go335
-rw-r--r--internal/processing/admin/domainpermission_test.go280
-rw-r--r--internal/processing/admin/util.go17
-rw-r--r--internal/typeutils/converter.go4
-rw-r--r--internal/typeutils/internaltofrontend.go37
37 files changed, 2266 insertions, 652 deletions
diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go
index 605c53731..3d8e88c42 100644
--- a/internal/api/client/admin/admin.go
+++ b/internal/api/client/admin/admin.go
@@ -31,6 +31,8 @@ const (
EmojiCategoriesPath = EmojiPath + "/categories"
DomainBlocksPath = BasePath + "/domain_blocks"
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey
+ DomainAllowsPath = BasePath + "/domain_allows"
+ DomainAllowsPathWithID = DomainAllowsPath + "/:" + IDKey
DomainKeysExpirePath = BasePath + "/domain_keys_expire"
AccountsPath = BasePath + "/accounts"
AccountsPathWithID = AccountsPath + "/:" + IDKey
@@ -84,6 +86,12 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
attachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler)
attachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler)
+ // domain allow stuff
+ attachHandler(http.MethodPost, DomainAllowsPath, m.DomainAllowsPOSTHandler)
+ attachHandler(http.MethodGet, DomainAllowsPath, m.DomainAllowsGETHandler)
+ attachHandler(http.MethodGet, DomainAllowsPathWithID, m.DomainAllowGETHandler)
+ attachHandler(http.MethodDelete, DomainAllowsPathWithID, m.DomainAllowDELETEHandler)
+
// domain maintenance stuff
attachHandler(http.MethodPost, DomainKeysExpirePath, m.DomainKeysExpirePOSTHandler)
diff --git a/internal/api/client/admin/domainallowcreate.go b/internal/api/client/admin/domainallowcreate.go
new file mode 100644
index 000000000..e8700f673
--- /dev/null
+++ b/internal/api/client/admin/domainallowcreate.go
@@ -0,0 +1,128 @@
+// 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 (
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// DomainAllowsPOSTHandler swagger:operation POST /api/v1/admin/domain_allows domainAllowCreate
+//
+// Create one or more domain allows, from a string or a file.
+//
+// You have two options when using this endpoint: either you can set `import` to `true` and
+// upload a file containing multiple domain allows, JSON-formatted, or you can leave import as
+// `false`, and just add one domain allow.
+//
+// The format of the json file should be something like: `[{"domain":"example.org"},{"domain":"whatever.com","public_comment":"they smell"}]`
+//
+// ---
+// tags:
+// - admin
+//
+// consumes:
+// - multipart/form-data
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: import
+// in: query
+// description: >-
+// Signal that a list of domain allows is being imported as a file.
+// If set to `true`, then 'domains' must be present as a JSON-formatted file.
+// If set to `false`, then `domains` will be ignored, and `domain` must be present.
+// type: boolean
+// default: false
+// -
+// name: domains
+// in: formData
+// description: >-
+// JSON-formatted list of domain allows to import.
+// This is only used if `import` is set to `true`.
+// type: file
+// -
+// name: domain
+// in: formData
+// description: >-
+// Single domain to allow.
+// Used only if `import` is not `true`.
+// type: string
+// -
+// name: obfuscate
+// in: formData
+// description: >-
+// Obfuscate the name of the domain when serving it publicly.
+// Eg., `example.org` becomes something like `ex***e.org`.
+// Used only if `import` is not `true`.
+// type: boolean
+// -
+// name: public_comment
+// in: formData
+// description: >-
+// Public comment about this domain allow.
+// This will be displayed alongside the domain allow if you choose to share allows.
+// Used only if `import` is not `true`.
+// type: string
+// -
+// name: private_comment
+// in: formData
+// description: >-
+// Private comment about this domain allow. Will only be shown to other admins, so this
+// is a useful way of internally keeping track of why a certain domain ended up allowed.
+// Used only if `import` is not `true`.
+// type: string
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: >-
+// The newly created domain allow, if `import` != `true`.
+// If a list has been imported, then an `array` of newly created domain allows will be returned instead.
+// schema:
+// "$ref": "#/definitions/domainPermission"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '409':
+// description: >-
+// Conflict: There is already an admin action running that conflicts with this action.
+// Check the error message in the response body for more information. This is a temporary
+// error; it should be possible to process this action if you try again in a bit.
+// '500':
+// description: internal server error
+func (m *Module) DomainAllowsPOSTHandler(c *gin.Context) {
+ m.createDomainPermissions(c,
+ gtsmodel.DomainPermissionAllow,
+ m.processor.Admin().DomainPermissionCreate,
+ m.processor.Admin().DomainPermissionsImport,
+ )
+}
diff --git a/internal/api/client/admin/domainallowdelete.go b/internal/api/client/admin/domainallowdelete.go
new file mode 100644
index 000000000..6237e403f
--- /dev/null
+++ b/internal/api/client/admin/domainallowdelete.go
@@ -0,0 +1,72 @@
+// 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 (
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// DomainAllowDELETEHandler swagger:operation DELETE /api/v1/admin/domain_allows/{id} domainAllowDelete
+//
+// Delete domain allow with the given ID.
+//
+// ---
+// tags:
+// - admin
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: The id of the domain allow.
+// in: path
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: The domain allow that was just deleted.
+// schema:
+// "$ref": "#/definitions/domainPermission"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '409':
+// description: >-
+// Conflict: There is already an admin action running that conflicts with this action.
+// Check the error message in the response body for more information. This is a temporary
+// error; it should be possible to process this action if you try again in a bit.
+// '500':
+// description: internal server error
+func (m *Module) DomainAllowDELETEHandler(c *gin.Context) {
+ m.deleteDomainPermission(c, gtsmodel.DomainPermissionAllow)
+}
diff --git a/internal/api/client/admin/domainallowget.go b/internal/api/client/admin/domainallowget.go
new file mode 100644
index 000000000..aa21743fa
--- /dev/null
+++ b/internal/api/client/admin/domainallowget.go
@@ -0,0 +1,67 @@
+// 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 (
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// DomainAllowGETHandler swagger:operation GET /api/v1/admin/domain_allows/{id} domainAllowGet
+//
+// View domain allow with the given ID.
+//
+// ---
+// tags:
+// - admin
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: The id of the domain allow.
+// in: path
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: The requested domain allow.
+// schema:
+// "$ref": "#/definitions/domainPermission"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) DomainAllowGETHandler(c *gin.Context) {
+ m.getDomainPermission(c, gtsmodel.DomainPermissionAllow)
+}
diff --git a/internal/api/client/admin/domainallowsget.go b/internal/api/client/admin/domainallowsget.go
new file mode 100644
index 000000000..6391c7138
--- /dev/null
+++ b/internal/api/client/admin/domainallowsget.go
@@ -0,0 +1,73 @@
+// 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 (
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// DomainAllowsGETHandler swagger:operation GET /api/v1/admin/domain_allows domainAllowsGet
+//
+// View all domain allows currently in place.
+//
+// ---
+// tags:
+// - admin
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: export
+// type: boolean
+// description: >-
+// If set to `true`, then each entry in the returned list of domain allows will only consist of
+// the fields `domain` and `public_comment`. This is perfect for when you want to save and share
+// a list of all the domains you have allowed on your instance, so that someone else can easily import them,
+// but you don't want them to see the database IDs of your allows, or private comments etc.
+// in: query
+// required: false
+//
+// security:
+// - OAuth2 Bearer:
+// - admin
+//
+// responses:
+// '200':
+// description: All domain allows currently in place.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/domainPermission"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) DomainAllowsGETHandler(c *gin.Context) {
+ m.getDomainPermissions(c, gtsmodel.DomainPermissionAllow)
+}
diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go
index 5cf9ea279..5234561cf 100644
--- a/internal/api/client/admin/domainblockcreate.go
+++ b/internal/api/client/admin/domainblockcreate.go
@@ -18,15 +18,8 @@
package admin
import (
- "errors"
- "fmt"
- "net/http"
-
"github.com/gin-gonic/gin"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// DomainBlocksPOSTHandler swagger:operation POST /api/v1/admin/domain_blocks domainBlockCreate
@@ -108,7 +101,7 @@ import (
// The newly created domain block, if `import` != `true`.
// If a list has been imported, then an `array` of newly created domain blocks will be returned instead.
// schema:
-// "$ref": "#/definitions/domainBlock"
+// "$ref": "#/definitions/domainPermission"
// '400':
// description: bad request
// '401':
@@ -127,108 +120,9 @@ import (
// '500':
// description: internal server error
func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
- authed, err := oauth.Authed(c, true, true, true, true)
- if err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if !*authed.User.Admin {
- err := fmt.Errorf("user %s not an admin", authed.User.ID)
- apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- importing, errWithCode := apiutil.ParseDomainBlockImport(c.Query(apiutil.DomainBlockImportKey), false)
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- form := new(apimodel.DomainBlockCreateRequest)
- if err := c.ShouldBind(form); err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if err := validateCreateDomainBlock(form, importing); err != nil {
- err := fmt.Errorf("error validating form: %w", err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if !importing {
- // Single domain block creation.
- domainBlock, _, errWithCode := m.processor.Admin().DomainBlockCreate(
- c.Request.Context(),
- authed.Account,
- form.Domain,
- form.Obfuscate,
- form.PublicComment,
- form.PrivateComment,
- "", // No sub ID for single block creation.
- )
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- c.JSON(http.StatusOK, domainBlock)
- return
- }
-
- // We're importing multiple domain blocks,
- // so we're looking at a multi-status response.
- multiStatus, errWithCode := m.processor.Admin().DomainBlocksImport(
- c.Request.Context(),
- authed.Account,
- form.Domains, // Pass the file through.
+ m.createDomainPermissions(c,
+ gtsmodel.DomainPermissionBlock,
+ m.processor.Admin().DomainPermissionCreate,
+ m.processor.Admin().DomainPermissionsImport,
)
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- // TODO: Return 207 and multiStatus data nicely
- // when supported by the admin panel.
-
- if multiStatus.Metadata.Failure != 0 {
- failures := make(map[string]any, multiStatus.Metadata.Failure)
- for _, entry := range multiStatus.Data {
- // nolint:forcetypeassert
- failures[entry.Resource.(string)] = entry.Message
- }
-
- err := fmt.Errorf("one or more errors importing domain blocks: %+v", failures)
- apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- // Success, return slice of domain blocks.
- domainBlocks := make([]any, 0, multiStatus.Metadata.Success)
- for _, entry := range multiStatus.Data {
- domainBlocks = append(domainBlocks, entry.Resource)
- }
-
- c.JSON(http.StatusOK, domainBlocks)
-}
-
-func validateCreateDomainBlock(form *apimodel.DomainBlockCreateRequest, imp bool) error {
- if imp {
- if form.Domains.Size == 0 {
- return errors.New("import was specified but list of domains is empty")
- }
- } else {
- // add some more validation here later if necessary
- if form.Domain == "" {
- return errors.New("empty domain provided")
- }
- }
-
- return nil
}
diff --git a/internal/api/client/admin/domainblockdelete.go b/internal/api/client/admin/domainblockdelete.go
index 9318bad87..a6f6619cd 100644
--- a/internal/api/client/admin/domainblockdelete.go
+++ b/internal/api/client/admin/domainblockdelete.go
@@ -18,14 +18,8 @@
package admin
import (
- "errors"
- "fmt"
- "net/http"
-
"github.com/gin-gonic/gin"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// DomainBlockDELETEHandler swagger:operation DELETE /api/v1/admin/domain_blocks/{id} domainBlockDelete
@@ -55,7 +49,7 @@ import (
// '200':
// description: The domain block that was just deleted.
// schema:
-// "$ref": "#/definitions/domainBlock"
+// "$ref": "#/definitions/domainPermission"
// '400':
// description: bad request
// '401':
@@ -74,35 +68,5 @@ import (
// '500':
// description: internal server error
func (m *Module) DomainBlockDELETEHandler(c *gin.Context) {
- authed, err := oauth.Authed(c, true, true, true, true)
- if err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if !*authed.User.Admin {
- err := fmt.Errorf("user %s not an admin", authed.User.ID)
- apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- domainBlockID := c.Param(IDKey)
- if domainBlockID == "" {
- err := errors.New("no domain block id specified")
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- domainBlock, _, errWithCode := m.processor.Admin().DomainBlockDelete(c.Request.Context(), authed.Account, domainBlockID)
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- c.JSON(http.StatusOK, domainBlock)
+ m.deleteDomainPermission(c, gtsmodel.DomainPermissionBlock)
}
diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go
index 87bb75a27..9e8d29905 100644
--- a/internal/api/client/admin/domainblockget.go
+++ b/internal/api/client/admin/domainblockget.go
@@ -18,13 +18,8 @@
package admin
import (
- "fmt"
- "net/http"
-
"github.com/gin-gonic/gin"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// DomainBlockGETHandler swagger:operation GET /api/v1/admin/domain_blocks/{id} domainBlockGet
@@ -54,7 +49,7 @@ import (
// '200':
// description: The requested domain block.
// schema:
-// "$ref": "#/definitions/domainBlock"
+// "$ref": "#/definitions/domainPermission"
// '400':
// description: bad request
// '401':
@@ -68,40 +63,5 @@ import (
// '500':
// description: internal server error
func (m *Module) DomainBlockGETHandler(c *gin.Context) {
- authed, err := oauth.Authed(c, true, true, true, true)
- if err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if !*authed.User.Admin {
- err := fmt.Errorf("user %s not an admin", authed.User.ID)
- apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- domainBlockID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- export, errWithCode := apiutil.ParseDomainBlockExport(c.Query(apiutil.DomainBlockExportKey), false)
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- domainBlock, errWithCode := m.processor.Admin().DomainBlockGet(c.Request.Context(), domainBlockID, export)
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- c.JSON(http.StatusOK, domainBlock)
+ m.getDomainPermission(c, gtsmodel.DomainPermissionBlock)
}
diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go
index 68947f471..bdcc03469 100644
--- a/internal/api/client/admin/domainblocksget.go
+++ b/internal/api/client/admin/domainblocksget.go
@@ -18,13 +18,8 @@
package admin
import (
- "fmt"
- "net/http"
-
"github.com/gin-gonic/gin"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// DomainBlocksGETHandler swagger:operation GET /api/v1/admin/domain_blocks domainBlocksGet
@@ -60,7 +55,7 @@ import (
// schema:
// type: array
// items:
-// "$ref": "#/definitions/domainBlock"
+// "$ref": "#/definitions/domainPermission"
// '400':
// description: bad request
// '401':
@@ -74,34 +69,5 @@ import (
// '500':
// description: internal server error
func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
- authed, err := oauth.Authed(c, true, true, true, true)
- if err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if !*authed.User.Admin {
- err := fmt.Errorf("user %s not an admin", authed.User.ID)
- apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
-
- export, errWithCode := apiutil.ParseDomainBlockExport(c.Query(apiutil.DomainBlockExportKey), false)
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- domainBlocks, errWithCode := m.processor.Admin().DomainBlocksGet(c.Request.Context(), authed.Account, export)
- if errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- c.JSON(http.StatusOK, domainBlocks)
+ m.getDomainPermissions(c, gtsmodel.DomainPermissionBlock)
}
diff --git a/internal/api/client/admin/domainpermission.go b/internal/api/client/admin/domainpermission.go
new file mode 100644
index 000000000..80aa05041
--- /dev/null
+++ b/internal/api/client/admin/domainpermission.go
@@ -0,0 +1,295 @@
+// 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"
+ "mime/multipart"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+type singleDomainPermCreate func(
+ context.Context,
+ gtsmodel.DomainPermissionType, // block/allow
+ *gtsmodel.Account, // admin account
+ string, // domain
+ bool, // obfuscate
+ string, // publicComment
+ string, // privateComment
+ string, // subscriptionID
+) (*apimodel.DomainPermission, string, gtserror.WithCode)
+
+type multiDomainPermCreate func(
+ context.Context,
+ gtsmodel.DomainPermissionType, // block/allow
+ *gtsmodel.Account, // admin account
+ *multipart.FileHeader, // domains
+) (*apimodel.MultiStatus, gtserror.WithCode)
+
+// createDomainPemissions either creates a single domain
+// permission entry (block/allow) or imports multiple domain
+// permission entries (multiple blocks, multiple allows)
+// using the given functions.
+//
+// Handling the creation of both types of permissions in
+// one function in this way reduces code duplication.
+func (m *Module) createDomainPermissions(
+ c *gin.Context,
+ permType gtsmodel.DomainPermissionType,
+ single singleDomainPermCreate,
+ multi multiDomainPermCreate,
+) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ importing, errWithCode := apiutil.ParseDomainPermissionImport(c.Query(apiutil.DomainPermissionImportKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // Parse + validate form.
+ form := new(apimodel.DomainPermissionRequest)
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if importing && form.Domains.Size == 0 {
+ err = errors.New("import was specified but list of domains is empty")
+ } else if form.Domain == "" {
+ err = errors.New("empty domain provided")
+ }
+
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !importing {
+ // Single domain permission creation.
+ domainBlock, _, errWithCode := single(
+ c.Request.Context(),
+ permType,
+ authed.Account,
+ form.Domain,
+ form.Obfuscate,
+ form.PublicComment,
+ form.PrivateComment,
+ "", // No sub ID for single perm creation.
+ )
+
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, domainBlock)
+ return
+ }
+
+ // We're importing multiple domain permissions,
+ // so we're looking at a multi-status response.
+ multiStatus, errWithCode := multi(
+ c.Request.Context(),
+ permType,
+ authed.Account,
+ form.Domains, // Pass the file through.
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ // TODO: Return 207 and multiStatus data nicely
+ // when supported by the admin panel.
+ if multiStatus.Metadata.Failure != 0 {
+ failures := make(map[string]any, multiStatus.Metadata.Failure)
+ for _, entry := range multiStatus.Data {
+ // nolint:forcetypeassert
+ failures[entry.Resource.(string)] = entry.Message
+ }
+
+ err := fmt.Errorf("one or more errors importing domain %ss: %+v", permType.String(), failures)
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ // Success, return slice of newly-created domain perms.
+ domainPerms := make([]any, 0, multiStatus.Metadata.Success)
+ for _, entry := range multiStatus.Data {
+ domainPerms = append(domainPerms, entry.Resource)
+ }
+
+ c.JSON(http.StatusOK, domainPerms)
+}
+
+// deleteDomainPermission deletes a single domain permission (block or allow).
+func (m *Module) deleteDomainPermission(
+ c *gin.Context,
+ permType gtsmodel.DomainPermissionType, // block/allow
+) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ domainPermID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ domainPerm, _, errWithCode := m.processor.Admin().DomainPermissionDelete(
+ c.Request.Context(),
+ permType,
+ authed.Account,
+ domainPermID,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, domainPerm)
+}
+
+// getDomainPermission gets a single domain permission (block or allow).
+func (m *Module) getDomainPermission(
+ c *gin.Context,
+ permType gtsmodel.DomainPermissionType,
+) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ domainPermID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ export, errWithCode := apiutil.ParseDomainPermissionExport(c.Query(apiutil.DomainPermissionExportKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ domainPerm, errWithCode := m.processor.Admin().DomainPermissionGet(
+ c.Request.Context(),
+ permType,
+ domainPermID,
+ export,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, domainPerm)
+}
+
+// getDomainPermissions gets all domain permissions of the given type (block, allow).
+func (m *Module) getDomainPermissions(
+ c *gin.Context,
+ permType gtsmodel.DomainPermissionType,
+) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if !*authed.User.Admin {
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ export, errWithCode := apiutil.ParseDomainPermissionExport(c.Query(apiutil.DomainPermissionExportKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ domainPerm, errWithCode := m.processor.Admin().DomainPermissionsGet(
+ c.Request.Context(),
+ permType,
+ authed.Account,
+ export,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, domainPerm)
+}
diff --git a/internal/api/model/domain.go b/internal/api/model/domain.go
index c5f77c82f..a5e1ddf10 100644
--- a/internal/api/model/domain.go
+++ b/internal/api/model/domain.go
@@ -37,46 +37,53 @@ type Domain struct {
PublicComment string `form:"public_comment" json:"public_comment,omitempty"`
}
-// DomainBlock represents a block on one domain
+// DomainPermission represents a permission applied to one domain (explicit block/allow).
//
-// swagger:model domainBlock
-type DomainBlock struct {
+// swagger:model domainPermission
+type DomainPermission struct {
Domain
- // The ID of the domain block.
+ // The ID of the domain permission entry.
// example: 01FBW21XJA09XYX51KV5JVBW0F
// readonly: true
ID string `json:"id,omitempty"`
- // Obfuscate the domain name when serving this domain block publicly.
- // A useful anti-harassment tool.
+ // Obfuscate the domain name when serving this domain permission entry publicly.
// example: false
Obfuscate bool `json:"obfuscate,omitempty"`
- // Private comment for this block, visible to our instance admins only.
+ // Private comment for this permission entry, visible to this instance's admins only.
// example: they are poopoo
PrivateComment string `json:"private_comment,omitempty"`
- // The ID of the subscription that created/caused this domain block.
+ // If applicable, the ID of the subscription that caused this domain permission entry to be created.
// example: 01FBW25TF5J67JW3HFHZCSD23K
SubscriptionID string `json:"subscription_id,omitempty"`
- // ID of the account that created this domain block.
+ // ID of the account that created this domain permission entry.
// example: 01FBW2758ZB6PBR200YPDDJK4C
CreatedBy string `json:"created_by,omitempty"`
- // Time at which this block was created (ISO 8601 Datetime).
+ // Time at which the permission entry was created (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
CreatedAt string `json:"created_at,omitempty"`
}
-// DomainBlockCreateRequest is the form submitted as a POST to /api/v1/admin/domain_blocks to create a new block.
+// DomainPermissionRequest is the form submitted as a POST to create a new domain permission entry (allow/block).
//
-// swagger:model domainBlockCreateRequest
-type DomainBlockCreateRequest struct {
- // A list of domains to block. Only used if import=true is specified.
+// swagger:model domainPermissionCreateRequest
+type DomainPermissionRequest struct {
+ // A list of domains for which this permission request should apply.
+ // Only used if import=true is specified.
Domains *multipart.FileHeader `form:"domains" json:"domains" xml:"domains"`
- // hostname/domain to block
+ // A single domain for which this permission request should apply.
+ // Only used if import=true is NOT specified or if import=false.
+ // example: example.org
Domain string `form:"domain" json:"domain" xml:"domain"`
- // whether the domain should be obfuscated when being displayed publicly
+ // Obfuscate the domain name when displaying this permission entry publicly.
+ // Ie., instead of 'example.org' show something like 'e**mpl*.or*'.
+ // example: false
Obfuscate bool `form:"obfuscate" json:"obfuscate" xml:"obfuscate"`
- // private comment for other admins on why the domain was blocked
+ // Private comment for other admins on why this permission entry was created.
+ // example: don't like 'em!!!!
PrivateComment string `form:"private_comment" json:"private_comment" xml:"private_comment"`
- // public comment on the reason for the domain block
+ // Public comment on why this permission entry was created.
+ // Will be visible to requesters at /api/v1/instance/peers if this endpoint is exposed.
+ // example: foss dorks 😫
PublicComment string `form:"public_comment" json:"public_comment" xml:"public_comment"`
}
diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go
index a87c77aeb..6a9116dcf 100644
--- a/internal/api/util/parsequery.go
+++ b/internal/api/util/parsequery.go
@@ -60,10 +60,10 @@ const (
WebUsernameKey = "username"
WebStatusIDKey = "status"
- /* Domain block keys */
+ /* Domain permission keys */
- DomainBlockExportKey = "export"
- DomainBlockImportKey = "import"
+ DomainPermissionExportKey = "export"
+ DomainPermissionImportKey = "import"
)
// parseError returns gtserror.WithCode set to 400 Bad Request, to indicate
@@ -121,12 +121,12 @@ func ParseSearchResolve(value string, defaultValue bool) (bool, gtserror.WithCod
return parseBool(value, defaultValue, SearchResolveKey)
}
-func ParseDomainBlockExport(value string, defaultValue bool) (bool, gtserror.WithCode) {
- return parseBool(value, defaultValue, DomainBlockExportKey)
+func ParseDomainPermissionExport(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ return parseBool(value, defaultValue, DomainPermissionExportKey)
}
-func ParseDomainBlockImport(value string, defaultValue bool) (bool, gtserror.WithCode) {
- return parseBool(value, defaultValue, DomainBlockImportKey)
+func ParseDomainPermissionImport(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ return parseBool(value, defaultValue, DomainPermissionImportKey)
}
/*
diff --git a/internal/cache/domain/domain.go b/internal/cache/domain/domain.go
index 37e97472a..051ec5c1b 100644
--- a/internal/cache/domain/domain.go
+++ b/internal/cache/domain/domain.go
@@ -26,23 +26,28 @@ import (
"golang.org/x/exp/slices"
)
-// BlockCache provides a means of caching domain blocks in memory to reduce load
-// on an underlying storage mechanism, e.g. a database.
+// Cache provides a means of caching domains in memory to reduce
+// load on an underlying storage mechanism, e.g. a database.
//
-// The in-memory block list is kept up-to-date by means of a passed loader function during every
-// call to .IsBlocked(). In the case of a nil internal block list, the loader function is called to
-// hydrate the cache with the latest list of domain blocks. The .Clear() function can be used to
-// invalidate the cache, e.g. when a domain block is added / deleted from the database.
-type BlockCache struct {
+// The in-memory domain list is kept up-to-date by means of a passed
+// loader function during every call to .Matches(). In the case of
+// a nil internal domain list, the loader function is called to hydrate
+// the cache with the latest list of domains.
+//
+// The .Clear() function can be used to invalidate the cache,
+// e.g. when an entry is added / deleted from the database.
+type Cache struct {
// atomically updated ptr value to the
- // current domain block cache radix trie.
+ // current domain cache radix trie.
rootptr unsafe.Pointer
}
-// IsBlocked checks whether domain is blocked. If the cache is not currently loaded, then the provided load function is used to hydrate it.
-func (b *BlockCache) IsBlocked(domain string, load func() ([]string, error)) (bool, error) {
+// Matches checks whether domain matches an entry in the cache.
+// If the cache is not currently loaded, then the provided load
+// function is used to hydrate it.
+func (c *Cache) Matches(domain string, load func() ([]string, error)) (bool, error) {
// Load the current root pointer value.
- ptr := atomic.LoadPointer(&b.rootptr)
+ ptr := atomic.LoadPointer(&c.rootptr)
if ptr == nil {
// Cache is not hydrated.
@@ -67,7 +72,7 @@ func (b *BlockCache) IsBlocked(domain string, load func() ([]string, error)) (bo
// Store the new node ptr.
ptr = unsafe.Pointer(root)
- atomic.StorePointer(&b.rootptr, ptr)
+ atomic.StorePointer(&c.rootptr, ptr)
}
// Look for a match in the trie node.
@@ -75,22 +80,20 @@ func (b *BlockCache) IsBlocked(domain string, load func() ([]string, error)) (bo
}
// Clear will drop the currently loaded domain list,
-// triggering a reload on next call to .IsBlocked().
-func (b *BlockCache) Clear() {
- atomic.StorePointer(&b.rootptr, nil)
+// triggering a reload on next call to .Matches().
+func (c *Cache) Clear() {
+ atomic.StorePointer(&c.rootptr, nil)
}
-// String returns a string representation of stored domains in block cache.
-func (b *BlockCache) String() string {
- if ptr := atomic.LoadPointer(&b.rootptr); ptr != nil {
+// String returns a string representation of stored domains in cache.
+func (c *Cache) String() string {
+ if ptr := atomic.LoadPointer(&c.rootptr); ptr != nil {
return (*root)(ptr).String()
}
return "<empty>"
}
-// root is the root node in the domain
-// block cache radix trie. this is the
-// singular access point to the trie.
+// root is the root node in the domain cache radix trie. this is the singular access point to the trie.
type root struct{ root node }
// Add will add the given domain to the radix trie.
@@ -99,14 +102,14 @@ func (r *root) Add(domain string) {
}
// Match will return whether the given domain matches
-// an existing stored domain block in this radix trie.
+// an existing stored domain in this radix trie.
func (r *root) Match(domain string) bool {
return r.root.match(strings.Split(domain, "."))
}
// Sort will sort the entire radix trie ensuring that
// child nodes are stored in alphabetical order. This
-// MUST be done to finalize the block cache in order
+// MUST be done to finalize the domain cache in order
// to speed up the binary search of node child parts.
func (r *root) Sort() {
r.root.sort()
@@ -154,7 +157,7 @@ func (n *node) add(parts []string) {
if len(parts) == 0 {
// Drop all children here as
- // this is a higher-level block
+ // this is a higher-level domain
// than that we previously had.
nn.child = nil
return
diff --git a/internal/cache/domain/domain_test.go b/internal/cache/domain/domain_test.go
index 8f975497b..9e091e1d0 100644
--- a/internal/cache/domain/domain_test.go
+++ b/internal/cache/domain/domain_test.go
@@ -24,21 +24,21 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/cache/domain"
)
-func TestBlockCache(t *testing.T) {
- c := new(domain.BlockCache)
+func TestCache(t *testing.T) {
+ c := new(domain.Cache)
- blocks := []string{
+ cachedDomains := []string{
"google.com",
"google.co.uk",
"pleroma.bad.host",
}
loader := func() ([]string, error) {
- t.Log("load: returning blocked domains")
- return blocks, nil
+ t.Log("load: returning cached domains")
+ return cachedDomains, nil
}
- // Check a list of known blocked domains.
+ // Check a list of known cached domains.
for _, domain := range []string{
"google.com",
"mail.google.com",
@@ -47,13 +47,13 @@ func TestBlockCache(t *testing.T) {
"pleroma.bad.host",
"dev.pleroma.bad.host",
} {
- t.Logf("checking domain is blocked: %s", domain)
- if b, _ := c.IsBlocked(domain, loader); !b {
- t.Errorf("domain should be blocked: %s", domain)
+ t.Logf("checking domain matches: %s", domain)
+ if b, _ := c.Matches(domain, loader); !b {
+ t.Errorf("domain should be matched: %s", domain)
}
}
- // Check a list of known unblocked domains.
+ // Check a list of known uncached domains.
for _, domain := range []string{
"askjeeves.com",
"ask-kim.co.uk",
@@ -62,9 +62,9 @@ func TestBlockCache(t *testing.T) {
"gts.bad.host",
"mastodon.bad.host",
} {
- t.Logf("checking domain isn't blocked: %s", domain)
- if b, _ := c.IsBlocked(domain, loader); b {
- t.Errorf("domain should not be blocked: %s", domain)
+ t.Logf("checking domain isn't matched: %s", domain)
+ if b, _ := c.Matches(domain, loader); b {
+ t.Errorf("domain should not be matched: %s", domain)
}
}
@@ -76,10 +76,10 @@ func TestBlockCache(t *testing.T) {
knownErr := errors.New("known error")
// Check that reload is actually performed and returns our error
- if _, err := c.IsBlocked("", func() ([]string, error) {
+ if _, err := c.Matches("", func() ([]string, error) {
t.Log("load: returning known error")
return nil, knownErr
}); !errors.Is(err, knownErr) {
- t.Errorf("is blocked did not return expected error: %v", err)
+ t.Errorf("matches did not return expected error: %v", err)
}
}
diff --git a/internal/cache/gts.go b/internal/cache/gts.go
index 12e917919..16a1585f7 100644
--- a/internal/cache/gts.go
+++ b/internal/cache/gts.go
@@ -36,7 +36,8 @@ type GTSCaches struct {
block *result.Cache[*gtsmodel.Block]
blockIDs *SliceCache[string]
boostOfIDs *SliceCache[string]
- domainBlock *domain.BlockCache
+ domainAllow *domain.Cache
+ domainBlock *domain.Cache
emoji *result.Cache[*gtsmodel.Emoji]
emojiCategory *result.Cache[*gtsmodel.EmojiCategory]
follow *result.Cache[*gtsmodel.Follow]
@@ -72,6 +73,7 @@ func (c *GTSCaches) Init() {
c.initBlock()
c.initBlockIDs()
c.initBoostOfIDs()
+ c.initDomainAllow()
c.initDomainBlock()
c.initEmoji()
c.initEmojiCategory()
@@ -139,8 +141,13 @@ func (c *GTSCaches) BoostOfIDs() *SliceCache[string] {
return c.boostOfIDs
}
+// DomainAllow provides access to the domain allow database cache.
+func (c *GTSCaches) DomainAllow() *domain.Cache {
+ return c.domainAllow
+}
+
// DomainBlock provides access to the domain block database cache.
-func (c *GTSCaches) DomainBlock() *domain.BlockCache {
+func (c *GTSCaches) DomainBlock() *domain.Cache {
return c.domainBlock
}
@@ -384,8 +391,12 @@ func (c *GTSCaches) initBoostOfIDs() {
)}
}
+func (c *GTSCaches) initDomainAllow() {
+ c.domainAllow = new(domain.Cache)
+}
+
func (c *GTSCaches) initDomainBlock() {
- c.domainBlock = new(domain.BlockCache)
+ c.domainBlock = new(domain.Cache)
}
func (c *GTSCaches) initEmoji() {
diff --git a/internal/config/config.go b/internal/config/config.go
index 16ef32a8b..314257831 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -76,12 +76,13 @@ type Configuration struct {
WebTemplateBaseDir string `name:"web-template-base-dir" usage:"Basedir for html templating files for rendering pages and composing emails."`
WebAssetBaseDir string `name:"web-asset-base-dir" usage:"Directory to serve static assets from, accessible at example.org/assets/"`
- InstanceExposePeers bool `name:"instance-expose-peers" usage:"Allow unauthenticated users to query /api/v1/instance/peers?filter=open"`
- InstanceExposeSuspended bool `name:"instance-expose-suspended" usage:"Expose suspended instances via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=suspended"`
- InstanceExposeSuspendedWeb bool `name:"instance-expose-suspended-web" usage:"Expose list of suspended instances as webpage on /about/suspended"`
- InstanceExposePublicTimeline bool `name:"instance-expose-public-timeline" usage:"Allow unauthenticated users to query /api/v1/timelines/public"`
- InstanceDeliverToSharedInboxes bool `name:"instance-deliver-to-shared-inboxes" usage:"Deliver federated messages to shared inboxes, if they're available."`
- InstanceInjectMastodonVersion bool `name:"instance-inject-mastodon-version" usage:"This injects a Mastodon compatible version in /api/v1/instance to help Mastodon clients that use that version for feature detection"`
+ InstanceFederationMode string `name:"instance-federation-mode" usage:"Set instance federation mode."`
+ InstanceExposePeers bool `name:"instance-expose-peers" usage:"Allow unauthenticated users to query /api/v1/instance/peers?filter=open"`
+ InstanceExposeSuspended bool `name:"instance-expose-suspended" usage:"Expose suspended instances via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=suspended"`
+ InstanceExposeSuspendedWeb bool `name:"instance-expose-suspended-web" usage:"Expose list of suspended instances as webpage on /about/suspended"`
+ InstanceExposePublicTimeline bool `name:"instance-expose-public-timeline" usage:"Allow unauthenticated users to query /api/v1/timelines/public"`
+ InstanceDeliverToSharedInboxes bool `name:"instance-deliver-to-shared-inboxes" usage:"Deliver federated messages to shared inboxes, if they're available."`
+ InstanceInjectMastodonVersion bool `name:"instance-inject-mastodon-version" usage:"This injects a Mastodon compatible version in /api/v1/instance to help Mastodon clients that use that version for feature detection"`
AccountsRegistrationOpen bool `name:"accounts-registration-open" usage:"Allow anyone to submit an account signup request. If false, server will be invite-only."`
AccountsApprovalRequired bool `name:"accounts-approval-required" usage:"Do account signups require approval by an admin or moderator before user can log in? If false, new registrations will be automatically approved."`
diff --git a/internal/config/const.go b/internal/config/const.go
new file mode 100644
index 000000000..29e4b14e8
--- /dev/null
+++ b/internal/config/const.go
@@ -0,0 +1,26 @@
+// 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 config
+
+// Instance federation mode determines how this
+// instance federates with others (if at all).
+const (
+ InstanceFederationModeBlocklist = "blocklist"
+ InstanceFederationModeAllowlist = "allowlist"
+ InstanceFederationModeDefault = InstanceFederationModeBlocklist
+)
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 9ad9c125c..fe2aa3acc 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -57,6 +57,7 @@ var Defaults = Configuration{
WebTemplateBaseDir: "./web/template/",
WebAssetBaseDir: "./web/assets/",
+ InstanceFederationMode: InstanceFederationModeDefault,
InstanceExposePeers: false,
InstanceExposeSuspended: false,
InstanceExposeSuspendedWeb: false,
diff --git a/internal/config/flags.go b/internal/config/flags.go
index 74ceedc00..29e0726a6 100644
--- a/internal/config/flags.go
+++ b/internal/config/flags.go
@@ -83,6 +83,7 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
cmd.Flags().String(WebAssetBaseDirFlag(), cfg.WebAssetBaseDir, fieldtag("WebAssetBaseDir", "usage"))
// Instance
+ cmd.Flags().String(InstanceFederationModeFlag(), cfg.InstanceFederationMode, fieldtag("InstanceFederationMode", "usage"))
cmd.Flags().Bool(InstanceExposePeersFlag(), cfg.InstanceExposePeers, fieldtag("InstanceExposePeers", "usage"))
cmd.Flags().Bool(InstanceExposeSuspendedFlag(), cfg.InstanceExposeSuspended, fieldtag("InstanceExposeSuspended", "usage"))
cmd.Flags().Bool(InstanceExposeSuspendedWebFlag(), cfg.InstanceExposeSuspendedWeb, fieldtag("InstanceExposeSuspendedWeb", "usage"))
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index f232d37a3..46a239596 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -749,6 +749,31 @@ func GetWebAssetBaseDir() string { return global.GetWebAssetBaseDir() }
// SetWebAssetBaseDir safely sets the value for global configuration 'WebAssetBaseDir' field
func SetWebAssetBaseDir(v string) { global.SetWebAssetBaseDir(v) }
+// GetInstanceFederationMode safely fetches the Configuration value for state's 'InstanceFederationMode' field
+func (st *ConfigState) GetInstanceFederationMode() (v string) {
+ st.mutex.RLock()
+ v = st.config.InstanceFederationMode
+ st.mutex.RUnlock()
+ return
+}
+
+// SetInstanceFederationMode safely sets the Configuration value for state's 'InstanceFederationMode' field
+func (st *ConfigState) SetInstanceFederationMode(v string) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.InstanceFederationMode = v
+ st.reloadToViper()
+}
+
+// InstanceFederationModeFlag returns the flag name for the 'InstanceFederationMode' field
+func InstanceFederationModeFlag() string { return "instance-federation-mode" }
+
+// GetInstanceFederationMode safely fetches the value for global configuration 'InstanceFederationMode' field
+func GetInstanceFederationMode() string { return global.GetInstanceFederationMode() }
+
+// SetInstanceFederationMode safely sets the value for global configuration 'InstanceFederationMode' field
+func SetInstanceFederationMode(v string) { global.SetInstanceFederationMode(v) }
+
// GetInstanceExposePeers safely fetches the Configuration value for state's 'InstanceExposePeers' field
func (st *ConfigState) GetInstanceExposePeers() (v bool) {
st.mutex.RLock()
diff --git a/internal/config/validate.go b/internal/config/validate.go
index bc8edc816..45cdc4eee 100644
--- a/internal/config/validate.go
+++ b/internal/config/validate.go
@@ -61,6 +61,17 @@ func Validate() error {
errs = append(errs, fmt.Errorf("%s must be set to either http or https, provided value was %s", ProtocolFlag(), proto))
}
+ // federation mode
+ switch federationMode := GetInstanceFederationMode(); federationMode {
+ case InstanceFederationModeBlocklist, InstanceFederationModeAllowlist:
+ // no problem
+ break
+ case "":
+ errs = append(errs, fmt.Errorf("%s must be set", InstanceFederationModeFlag()))
+ default:
+ errs = append(errs, fmt.Errorf("%s must be set to either blocklist or allowlist, provided value was %s", InstanceFederationModeFlag(), federationMode))
+ }
+
webAssetsBaseDir := GetWebAssetBaseDir()
if webAssetsBaseDir == "" {
errs = append(errs, fmt.Errorf("%s must be set", WebAssetBaseDirFlag()))
diff --git a/internal/db/bundb/domain.go b/internal/db/bundb/domain.go
index c989d4fe4..dd626bc0a 100644
--- a/internal/db/bundb/domain.go
+++ b/internal/db/bundb/domain.go
@@ -23,6 +23,7 @@ import (
"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/state"
"github.com/superseriousbusiness/gotosocial/internal/util"
@@ -34,6 +35,102 @@ type domainDB struct {
state *state.State
}
+func (d *domainDB) CreateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) error {
+ // Normalize the domain as punycode
+ var err error
+ allow.Domain, err = util.Punify(allow.Domain)
+ if err != nil {
+ return err
+ }
+
+ // Attempt to store domain allow in DB
+ if _, err := d.db.NewInsert().
+ Model(allow).
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Clear the domain allow cache (for later reload)
+ d.state.Caches.GTS.DomainAllow().Clear()
+
+ return nil
+}
+
+func (d *domainDB) GetDomainAllow(ctx context.Context, domain string) (*gtsmodel.DomainAllow, error) {
+ // Normalize the domain as punycode
+ domain, err := util.Punify(domain)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check for easy case, domain referencing *us*
+ if domain == "" || domain == config.GetAccountDomain() ||
+ domain == config.GetHost() {
+ return nil, db.ErrNoEntries
+ }
+
+ var allow gtsmodel.DomainAllow
+
+ // Look for allow matching domain in DB
+ q := d.db.
+ NewSelect().
+ Model(&allow).
+ Where("? = ?", bun.Ident("domain_allow.domain"), domain)
+ if err := q.Scan(ctx); err != nil {
+ return nil, err
+ }
+
+ return &allow, nil
+}
+
+func (d *domainDB) GetDomainAllows(ctx context.Context) ([]*gtsmodel.DomainAllow, error) {
+ allows := []*gtsmodel.DomainAllow{}
+
+ if err := d.db.
+ NewSelect().
+ Model(&allows).
+ Scan(ctx); err != nil {
+ return nil, err
+ }
+
+ return allows, nil
+}
+
+func (d *domainDB) GetDomainAllowByID(ctx context.Context, id string) (*gtsmodel.DomainAllow, error) {
+ var allow gtsmodel.DomainAllow
+
+ q := d.db.
+ NewSelect().
+ Model(&allow).
+ Where("? = ?", bun.Ident("domain_allow.id"), id)
+ if err := q.Scan(ctx); err != nil {
+ return nil, err
+ }
+
+ return &allow, nil
+}
+
+func (d *domainDB) DeleteDomainAllow(ctx context.Context, domain string) error {
+ // Normalize the domain as punycode
+ domain, err := util.Punify(domain)
+ if err != nil {
+ return err
+ }
+
+ // Attempt to delete domain allow
+ if _, err := d.db.NewDelete().
+ Model((*gtsmodel.DomainAllow)(nil)).
+ Where("? = ?", bun.Ident("domain_allow.domain"), domain).
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Clear the domain allow cache (for later reload)
+ d.state.Caches.GTS.DomainAllow().Clear()
+
+ return nil
+}
+
func (d *domainDB) CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error {
// Normalize the domain as punycode
var err error
@@ -137,14 +234,32 @@ func (d *domainDB) IsDomainBlocked(ctx context.Context, domain string) (bool, er
return false, err
}
- // Check for easy case, domain referencing *us*
+ // Domain referencing *us* cannot be blocked.
if domain == "" || domain == config.GetAccountDomain() ||
domain == config.GetHost() {
return false, nil
}
+ // Check the cache for an explicit domain allow (hydrating the cache with callback if necessary).
+ explicitAllow, err := d.state.Caches.GTS.DomainAllow().Matches(domain, func() ([]string, error) {
+ var domains []string
+
+ // Scan list of all explicitly allowed domains from DB
+ q := d.db.NewSelect().
+ Table("domain_allows").
+ Column("domain")
+ if err := q.Scan(ctx, &domains); err != nil {
+ return nil, err
+ }
+
+ return domains, nil
+ })
+ if err != nil {
+ return false, err
+ }
+
// Check the cache for a domain block (hydrating the cache with callback if necessary)
- return d.state.Caches.GTS.DomainBlock().IsBlocked(domain, func() ([]string, error) {
+ explicitBlock, err := d.state.Caches.GTS.DomainBlock().Matches(domain, func() ([]string, error) {
var domains []string
// Scan list of all blocked domains from DB
@@ -157,6 +272,35 @@ func (d *domainDB) IsDomainBlocked(ctx context.Context, domain string) (bool, er
return domains, nil
})
+ if err != nil {
+ return false, err
+ }
+
+ // Calculate if blocked
+ // based on federation mode.
+ switch mode := config.GetInstanceFederationMode(); mode {
+
+ case config.InstanceFederationModeBlocklist:
+ // Blocklist/default mode: explicit allow
+ // takes precedence over explicit block.
+ //
+ // Domains that have neither block
+ // or allow entries are allowed.
+ return !(explicitAllow || !explicitBlock), nil
+
+ case config.InstanceFederationModeAllowlist:
+ // Allowlist mode: explicit block takes
+ // precedence over explicit allow.
+ //
+ // Domains that have neither block
+ // or allow entries are blocked.
+ return (explicitBlock || !explicitAllow), nil
+
+ default:
+ // This should never happen but account
+ // for it anyway to make the code tidier.
+ return false, gtserror.Newf("unrecognized federation mode: %s", mode)
+ }
}
func (d *domainDB) AreDomainsBlocked(ctx context.Context, domains []string) (bool, error) {
diff --git a/internal/db/bundb/domain_test.go b/internal/db/bundb/domain_test.go
index e4e199fa1..ff687cf59 100644
--- a/internal/db/bundb/domain_test.go
+++ b/internal/db/bundb/domain_test.go
@@ -55,6 +55,59 @@ func (suite *DomainTestSuite) TestIsDomainBlocked() {
suite.WithinDuration(time.Now(), domainBlock.CreatedAt, 10*time.Second)
}
+func (suite *DomainTestSuite) TestIsDomainBlockedWithAllow() {
+ ctx := context.Background()
+
+ domainBlock := &gtsmodel.DomainBlock{
+ ID: "01G204214Y9TNJEBX39C7G88SW",
+ Domain: "some.bad.apples",
+ CreatedByAccountID: suite.testAccounts["admin_account"].ID,
+ CreatedByAccount: suite.testAccounts["admin_account"],
+ }
+
+ // no domain block exists for the given domain yet
+ blocked, err := suite.db.IsDomainBlocked(ctx, domainBlock.Domain)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.False(blocked)
+
+ // Block this domain.
+ if err := suite.db.CreateDomainBlock(ctx, domainBlock); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // domain block now exists
+ blocked, err = suite.db.IsDomainBlocked(ctx, domainBlock.Domain)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.True(blocked)
+ suite.WithinDuration(time.Now(), domainBlock.CreatedAt, 10*time.Second)
+
+ // Explicitly allow this domain.
+ domainAllow := &gtsmodel.DomainAllow{
+ ID: "01H8KY9MJQFWE712EG3VN02Y3J",
+ Domain: "some.bad.apples",
+ CreatedByAccountID: suite.testAccounts["admin_account"].ID,
+ CreatedByAccount: suite.testAccounts["admin_account"],
+ }
+
+ if err := suite.db.CreateDomainAllow(ctx, domainAllow); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Domain allow now exists
+ blocked, err = suite.db.IsDomainBlocked(ctx, domainBlock.Domain)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.False(blocked)
+}
+
func (suite *DomainTestSuite) TestIsDomainBlockedWildcard() {
ctx := context.Background()
diff --git a/internal/db/bundb/migrations/20230908083121_allowlist.go.go b/internal/db/bundb/migrations/20230908083121_allowlist.go.go
new file mode 100644
index 000000000..2d86f8c03
--- /dev/null
+++ b/internal/db/bundb/migrations/20230908083121_allowlist.go.go
@@ -0,0 +1,62 @@
+// 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 migrations
+
+import (
+ "context"
+
+ gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ // Create domain allow.
+ if _, err := tx.
+ NewCreateTable().
+ Model(&gtsmodel.DomainAllow{}).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Index domain allow.
+ if _, err := tx.
+ NewCreateIndex().
+ Table("domain_allows").
+ Index("domain_allows_domain_idx").
+ Column("domain").
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/domain.go b/internal/db/domain.go
index 740ccefe6..3f7803d62 100644
--- a/internal/db/domain.go
+++ b/internal/db/domain.go
@@ -26,6 +26,25 @@ import (
// Domain contains DB functions related to domains and domain blocks.
type Domain interface {
+ /*
+ Block/allow storage + retrieval functions.
+ */
+
+ // CreateDomainAllow puts the given instance-level domain allow into the database.
+ CreateDomainAllow(ctx context.Context, allow *gtsmodel.DomainAllow) error
+
+ // GetDomainAllow returns one instance-level domain allow with the given domain, if it exists.
+ GetDomainAllow(ctx context.Context, domain string) (*gtsmodel.DomainAllow, error)
+
+ // GetDomainAllowByID returns one instance-level domain allow with the given id, if it exists.
+ GetDomainAllowByID(ctx context.Context, id string) (*gtsmodel.DomainAllow, error)
+
+ // GetDomainAllows returns all instance-level domain allows currently enforced by this instance.
+ GetDomainAllows(ctx context.Context) ([]*gtsmodel.DomainAllow, error)
+
+ // DeleteDomainAllow deletes an instance-level domain allow with the given domain, if it exists.
+ DeleteDomainAllow(ctx context.Context, domain string) error
+
// CreateDomainBlock puts the given instance-level domain block into the database.
CreateDomainBlock(ctx context.Context, block *gtsmodel.DomainBlock) error
@@ -41,15 +60,22 @@ type Domain interface {
// DeleteDomainBlock deletes an instance-level domain block with the given domain, if it exists.
DeleteDomainBlock(ctx context.Context, domain string) error
- // IsDomainBlocked checks if an instance-level domain block exists for the given domain string (eg., `example.org`).
+ /*
+ Block/allow checking functions.
+ */
+
+ // IsDomainBlocked checks if domain is blocked, accounting for both explicit allows and blocks.
+ // Will check allows first, so an allowed domain will always return false, even if it's also blocked.
IsDomainBlocked(ctx context.Context, domain string) (bool, error)
- // AreDomainsBlocked checks if an instance-level domain block exists for any of the given domains strings, and returns true if even one is found.
+ // AreDomainsBlocked calls IsDomainBlocked for each domain.
+ // Will return true if even one of the given domains is blocked.
AreDomainsBlocked(ctx context.Context, domains []string) (bool, error)
- // IsURIBlocked checks if an instance-level domain block exists for the `host` in the given URI (eg., `https://example.org/users/whatever`).
+ // IsURIBlocked calls IsDomainBlocked for the host of the given URI.
IsURIBlocked(ctx context.Context, uri *url.URL) (bool, error)
- // AreURIsBlocked checks if an instance-level domain block exists for any `host` in the given URI slice, and returns true if even one is found.
+ // AreURIsBlocked calls IsURIBlocked for each URI.
+ // Will return true if even one of the given URIs is blocked.
AreURIsBlocked(ctx context.Context, uris []*url.URL) (bool, error)
}
diff --git a/internal/gtsmodel/adminaction.go b/internal/gtsmodel/adminaction.go
index 1e55a33f9..e8b82e495 100644
--- a/internal/gtsmodel/adminaction.go
+++ b/internal/gtsmodel/adminaction.go
@@ -42,7 +42,7 @@ func (c AdminActionCategory) String() string {
case AdminActionCategoryDomain:
return "domain"
default:
- return "unknown"
+ return "unknown" //nolint:goconst
}
}
diff --git a/internal/gtsmodel/domainallow.go b/internal/gtsmodel/domainallow.go
new file mode 100644
index 000000000..2a3e53e79
--- /dev/null
+++ b/internal/gtsmodel/domainallow.go
@@ -0,0 +1,78 @@
+// 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 gtsmodel
+
+import "time"
+
+// DomainAllow represents a federation allow towards a particular domain.
+type DomainAllow struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
+ UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
+ Domain string `bun:",nullzero,notnull"` // domain to allow. Eg. 'whatever.com'
+ CreatedByAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // Account ID of the creator of this allow
+ CreatedByAccount *Account `bun:"rel:belongs-to"` // Account corresponding to createdByAccountID
+ PrivateComment string `bun:""` // Private comment on this allow, viewable to admins
+ PublicComment string `bun:""` // Public comment on this allow, viewable (optionally) by everyone
+ Obfuscate *bool `bun:",nullzero,notnull,default:false"` // whether the domain name should appear obfuscated when displaying it publicly
+ SubscriptionID string `bun:"type:CHAR(26),nullzero"` // if this allow was created through a subscription, what's the subscription ID?
+}
+
+func (d *DomainAllow) GetID() string {
+ return d.ID
+}
+
+func (d *DomainAllow) GetCreatedAt() time.Time {
+ return d.CreatedAt
+}
+
+func (d *DomainAllow) GetUpdatedAt() time.Time {
+ return d.UpdatedAt
+}
+
+func (d *DomainAllow) GetDomain() string {
+ return d.Domain
+}
+
+func (d *DomainAllow) GetCreatedByAccountID() string {
+ return d.CreatedByAccountID
+}
+
+func (d *DomainAllow) GetCreatedByAccount() *Account {
+ return d.CreatedByAccount
+}
+
+func (d *DomainAllow) GetPrivateComment() string {
+ return d.PrivateComment
+}
+
+func (d *DomainAllow) GetPublicComment() string {
+ return d.PublicComment
+}
+
+func (d *DomainAllow) GetObfuscate() *bool {
+ return d.Obfuscate
+}
+
+func (d *DomainAllow) GetSubscriptionID() string {
+ return d.SubscriptionID
+}
+
+func (d *DomainAllow) GetType() DomainPermissionType {
+ return DomainPermissionAllow
+}
diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go
index dfe642ef5..4e0b3ca65 100644
--- a/internal/gtsmodel/domainblock.go
+++ b/internal/gtsmodel/domainblock.go
@@ -32,3 +32,47 @@ type DomainBlock struct {
Obfuscate *bool `bun:",nullzero,notnull,default:false"` // whether the domain name should appear obfuscated when displaying it publicly
SubscriptionID string `bun:"type:CHAR(26),nullzero"` // if this block was created through a subscription, what's the subscription ID?
}
+
+func (d *DomainBlock) GetID() string {
+ return d.ID
+}
+
+func (d *DomainBlock) GetCreatedAt() time.Time {
+ return d.CreatedAt
+}
+
+func (d *DomainBlock) GetUpdatedAt() time.Time {
+ return d.UpdatedAt
+}
+
+func (d *DomainBlock) GetDomain() string {
+ return d.Domain
+}
+
+func (d *DomainBlock) GetCreatedByAccountID() string {
+ return d.CreatedByAccountID
+}
+
+func (d *DomainBlock) GetCreatedByAccount() *Account {
+ return d.CreatedByAccount
+}
+
+func (d *DomainBlock) GetPrivateComment() string {
+ return d.PrivateComment
+}
+
+func (d *DomainBlock) GetPublicComment() string {
+ return d.PublicComment
+}
+
+func (d *DomainBlock) GetObfuscate() *bool {
+ return d.Obfuscate
+}
+
+func (d *DomainBlock) GetSubscriptionID() string {
+ return d.SubscriptionID
+}
+
+func (d *DomainBlock) GetType() DomainPermissionType {
+ return DomainPermissionBlock
+}
diff --git a/internal/gtsmodel/domainpermission.go b/internal/gtsmodel/domainpermission.go
new file mode 100644
index 000000000..01e8fdaaa
--- /dev/null
+++ b/internal/gtsmodel/domainpermission.go
@@ -0,0 +1,67 @@
+// 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 gtsmodel
+
+import "time"
+
+// DomainPermission models a domain
+// permission entry (block/allow).
+type DomainPermission interface {
+ GetID() string
+ GetCreatedAt() time.Time
+ GetUpdatedAt() time.Time
+ GetDomain() string
+ GetCreatedByAccountID() string
+ GetCreatedByAccount() *Account
+ GetPrivateComment() string
+ GetPublicComment() string
+ GetObfuscate() *bool
+ GetSubscriptionID() string
+ GetType() DomainPermissionType
+}
+
+// Domain permission type.
+type DomainPermissionType uint8
+
+const (
+ DomainPermissionUnknown DomainPermissionType = iota
+ DomainPermissionBlock // Explicitly block a domain.
+ DomainPermissionAllow // Explicitly allow a domain.
+)
+
+func (p DomainPermissionType) String() string {
+ switch p {
+ case DomainPermissionBlock:
+ return "block"
+ case DomainPermissionAllow:
+ return "allow"
+ default:
+ return "unknown"
+ }
+}
+
+func NewDomainPermissionType(in string) DomainPermissionType {
+ switch in {
+ case "block":
+ return DomainPermissionBlock
+ case "allow":
+ return DomainPermissionAllow
+ default:
+ return DomainPermissionUnknown
+ }
+}
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 = &gtsmodel.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,
+ &gtsmodel.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,
+ &gtsmodel.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,
- &gtsmodel.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,
+ &gtsmodel.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.
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 774b68157..af77734cc 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -91,8 +91,8 @@ type TypeConverter interface {
RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error)
// NotificationToAPINotification converts a gts notification into a api notification
NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification) (*apimodel.Notification, error)
- // DomainBlockToAPIDomainBlock converts a gts model domin block into a api domain block, for serving at /api/v1/admin/domain_blocks
- DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error)
+ // DomainPermToAPIDomainPerm converts a gts model domin block or allow into an api domain permission.
+ DomainPermToAPIDomainPerm(ctx context.Context, d gtsmodel.DomainPermission, export bool) (*apimodel.DomainPermission, error)
// ReportToAPIReport converts a gts model report into an api model report, for serving at /api/v1/reports
ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (*apimodel.Report, error)
// ReportToAdminAPIReport converts a gts model report into an admin view report, for serving at /api/v1/admin/reports
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 050997bda..11838e2bd 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -1041,32 +1041,39 @@ func (c *converter) NotificationToAPINotification(ctx context.Context, n *gtsmod
}, nil
}
-func (c *converter) DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error) {
+func (c *converter) DomainPermToAPIDomainPerm(
+ ctx context.Context,
+ d gtsmodel.DomainPermission,
+ export bool,
+) (*apimodel.DomainPermission, error) {
// Domain may be in Punycode,
// de-punify it just in case.
- d, err := util.DePunify(b.Domain)
+ domain, err := util.DePunify(d.GetDomain())
if err != nil {
- return nil, fmt.Errorf("DomainBlockToAPIDomainBlock: error de-punifying domain %s: %w", b.Domain, err)
+ return nil, gtserror.Newf("error de-punifying domain %s: %w", d.GetDomain(), err)
}
- domainBlock := &apimodel.DomainBlock{
+ domainPerm := &apimodel.DomainPermission{
Domain: apimodel.Domain{
- Domain: d,
- PublicComment: b.PublicComment,
+ Domain: domain,
+ PublicComment: d.GetPublicComment(),
},
}
- // if we're exporting a domain block, return it with minimal information attached
- if !export {
- domainBlock.ID = b.ID
- domainBlock.Obfuscate = *b.Obfuscate
- domainBlock.PrivateComment = b.PrivateComment
- domainBlock.SubscriptionID = b.SubscriptionID
- domainBlock.CreatedBy = b.CreatedByAccountID
- domainBlock.CreatedAt = util.FormatISO8601(b.CreatedAt)
+ // If we're exporting, provide
+ // only bare minimum detail.
+ if export {
+ return domainPerm, nil
}
- return domainBlock, nil
+ domainPerm.ID = d.GetID()
+ domainPerm.Obfuscate = *d.GetObfuscate()
+ domainPerm.PrivateComment = d.GetPrivateComment()
+ domainPerm.SubscriptionID = d.GetSubscriptionID()
+ domainPerm.CreatedBy = d.GetCreatedByAccountID()
+ domainPerm.CreatedAt = util.FormatISO8601(d.GetCreatedAt())
+
+ return domainPerm, nil
}
func (c *converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (*apimodel.Report, error) {