diff options
Diffstat (limited to 'internal/api')
-rw-r--r-- | internal/api/client/admin/admin.go | 8 | ||||
-rw-r--r-- | internal/api/client/admin/domainallowcreate.go | 128 | ||||
-rw-r--r-- | internal/api/client/admin/domainallowdelete.go | 72 | ||||
-rw-r--r-- | internal/api/client/admin/domainallowget.go | 67 | ||||
-rw-r--r-- | internal/api/client/admin/domainallowsget.go | 73 | ||||
-rw-r--r-- | internal/api/client/admin/domainblockcreate.go | 118 | ||||
-rw-r--r-- | internal/api/client/admin/domainblockdelete.go | 42 | ||||
-rw-r--r-- | internal/api/client/admin/domainblockget.go | 46 | ||||
-rw-r--r-- | internal/api/client/admin/domainblocksget.go | 40 | ||||
-rw-r--r-- | internal/api/client/admin/domainpermission.go | 295 | ||||
-rw-r--r-- | internal/api/model/domain.go | 43 | ||||
-rw-r--r-- | internal/api/util/parsequery.go | 14 |
12 files changed, 690 insertions, 256 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) } /* |