summaryrefslogtreecommitdiff
path: root/internal/federation/dereferencing
diff options
context:
space:
mode:
Diffstat (limited to 'internal/federation/dereferencing')
-rw-r--r--internal/federation/dereferencing/announce.go24
-rw-r--r--internal/federation/dereferencing/dereferencer.go11
-rw-r--r--internal/federation/dereferencing/dereferencer_test.go14
-rw-r--r--internal/federation/dereferencing/status.go83
-rw-r--r--internal/federation/dereferencing/status_permitted.go216
5 files changed, 257 insertions, 91 deletions
diff --git a/internal/federation/dereferencing/announce.go b/internal/federation/dereferencing/announce.go
index d786d0695..a3eaf199d 100644
--- a/internal/federation/dereferencing/announce.go
+++ b/internal/federation/dereferencing/announce.go
@@ -69,12 +69,6 @@ func (d *Dereferencer) EnrichAnnounce(
return nil, err
}
- // Generate an ID for the boost wrapper status.
- boost.ID, err = id.NewULIDFromTime(boost.CreatedAt)
- if err != nil {
- return nil, gtserror.Newf("error generating id: %w", err)
- }
-
// Set boost_of_uri again in case the
// original URI was an indirect link.
boost.BoostOfURI = target.URI
@@ -92,6 +86,24 @@ func (d *Dereferencer) EnrichAnnounce(
boost.Visibility = target.Visibility
boost.Federated = target.Federated
+ // Ensure this Announce is permitted by the Announcee.
+ permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost)
+ if err != nil {
+ return nil, gtserror.Newf("error checking permitted status %s: %w", boost.URI, err)
+ }
+
+ if !permit {
+ // Return a checkable error type that can be ignored.
+ err := gtserror.Newf("dropping unpermitted status: %s", boost.URI)
+ return nil, gtserror.SetNotPermitted(err)
+ }
+
+ // Generate an ID for the boost wrapper status.
+ boost.ID, err = id.NewULIDFromTime(boost.CreatedAt)
+ if err != nil {
+ return nil, gtserror.Newf("error generating id: %w", err)
+ }
+
// Store the boost wrapper status in database.
switch err = d.state.DB.PutStatus(ctx, boost); {
case err == nil:
diff --git a/internal/federation/dereferencing/dereferencer.go b/internal/federation/dereferencing/dereferencer.go
index bcc145c27..3bff0d1a2 100644
--- a/internal/federation/dereferencing/dereferencer.go
+++ b/internal/federation/dereferencing/dereferencer.go
@@ -22,6 +22,7 @@ import (
"sync"
"time"
+ "github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/state"
@@ -83,7 +84,8 @@ type Dereferencer struct {
converter *typeutils.Converter
transportController transport.Controller
mediaManager *media.Manager
- visibility *visibility.Filter
+ visFilter *visibility.Filter
+ intFilter *interaction.Filter
// in-progress dereferencing media / emoji
derefMedia map[string]*media.ProcessingMedia
@@ -102,12 +104,14 @@ type Dereferencer struct {
handshakesMu sync.Mutex
}
-// NewDereferencer returns a Dereferencer initialized with the given parameters.
+// NewDereferencer returns a Dereferencer
+// initialized with the given parameters.
func NewDereferencer(
state *state.State,
converter *typeutils.Converter,
transportController transport.Controller,
visFilter *visibility.Filter,
+ intFilter *interaction.Filter,
mediaManager *media.Manager,
) Dereferencer {
return Dereferencer{
@@ -115,7 +119,8 @@ func NewDereferencer(
converter: converter,
transportController: transportController,
mediaManager: mediaManager,
- visibility: visFilter,
+ visFilter: visFilter,
+ intFilter: intFilter,
derefMedia: make(map[string]*media.ProcessingMedia),
derefEmojis: make(map[string]*media.ProcessingEmoji),
handshakes: make(map[string][]*url.URL),
diff --git a/internal/federation/dereferencing/dereferencer_test.go b/internal/federation/dereferencing/dereferencer_test.go
index 293118167..f00e876ae 100644
--- a/internal/federation/dereferencing/dereferencer_test.go
+++ b/internal/federation/dereferencing/dereferencer_test.go
@@ -22,6 +22,7 @@ import (
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
+ "github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/state"
@@ -79,8 +80,19 @@ func (suite *DereferencerStandardTestSuite) SetupTest() {
suite.state.Storage = suite.storage
visFilter := visibility.NewFilter(&suite.state)
+ intFilter := interaction.NewFilter(&suite.state)
media := testrig.NewTestMediaManager(&suite.state)
- suite.dereferencer = dereferencing.NewDereferencer(&suite.state, converter, testrig.NewTestTransportController(&suite.state, suite.client), visFilter, media)
+ suite.dereferencer = dereferencing.NewDereferencer(
+ &suite.state,
+ converter,
+ testrig.NewTestTransportController(
+ &suite.state,
+ suite.client,
+ ),
+ visFilter,
+ intFilter,
+ media,
+ )
testrig.StandardDBSetup(suite.db, nil)
}
diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go
index 0e227a0c1..88746fc3a 100644
--- a/internal/federation/dereferencing/status.go
+++ b/internal/federation/dereferencing/status.go
@@ -502,7 +502,8 @@ func (d *Dereferencer) enrichStatus(
latestStatus.Local = status.Local
// Check if this is a permitted status we should accept.
- permit, err := d.isPermittedStatus(ctx, status, latestStatus)
+ // Function also sets "PendingApproval" bool as necessary.
+ permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus)
if err != nil {
return nil, nil, gtserror.Newf("error checking permissibility for status %s: %w", uri, err)
}
@@ -560,86 +561,6 @@ func (d *Dereferencer) enrichStatus(
return latestStatus, apubStatus, nil
}
-// isPermittedStatus returns whether the given status
-// is permitted to be stored on this instance, checking
-// whether the author is suspended, and passes visibility
-// checks against status being replied-to (if any).
-func (d *Dereferencer) isPermittedStatus(
- ctx context.Context,
- existing *gtsmodel.Status,
- status *gtsmodel.Status,
-) (
- permitted bool, // is permitted?
- err error,
-) {
-
- // our failure condition handling
- // at the end of this function for
- // the case of permission = false.
- onFail := func() (bool, error) {
- if existing != nil {
- log.Infof(ctx, "deleting unpermitted: %s", existing.URI)
-
- // Delete existing status from database as it's no longer permitted.
- if err := d.state.DB.DeleteStatusByID(ctx, existing.ID); err != nil {
- log.Errorf(ctx, "error deleting %s after permissivity fail: %v", existing.URI, err)
- }
- }
- return false, nil
- }
-
- if !status.Account.SuspendedAt.IsZero() {
- // The status author is suspended,
- // this shouldn't have reached here
- // but it's a fast check anyways.
- return onFail()
- }
-
- if status.InReplyToURI == "" {
- // This status isn't in
- // reply to anything!
- return true, nil
- }
-
- if status.InReplyTo == nil {
- // If no inReplyTo has been set,
- // we return here for now as we
- // can't perform further checks.
- //
- // Worst case we allow something
- // through, and later on during
- // refetch it will get deleted.
- return true, nil
- }
-
- if status.InReplyTo.BoostOfID != "" {
- // We do not permit replies to
- // boost wrapper statuses. (this
- // shouldn't be able to happen).
- return onFail()
- }
-
- // Default to true
- permitted = true
-
- if *status.InReplyTo.Local {
- // Check visibility of inReplyTo to status author.
- permitted, err = d.visibility.StatusVisible(ctx,
- status.Account,
- status.InReplyTo,
- )
- if err != nil {
- return false, gtserror.Newf("error checking in-reply-to visibility: %w", err)
- }
- }
-
- if permitted {
- return true, nil
- }
-
- return onFail()
-}
-
func (d *Dereferencer) fetchStatusMentions(
ctx context.Context,
requestUser string,
diff --git a/internal/federation/dereferencing/status_permitted.go b/internal/federation/dereferencing/status_permitted.go
new file mode 100644
index 000000000..5c16f9f15
--- /dev/null
+++ b/internal/federation/dereferencing/status_permitted.go
@@ -0,0 +1,216 @@
+// 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 dereferencing
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+)
+
+// isPermittedStatus returns whether the given status
+// is permitted to be stored on this instance, checking:
+//
+// - author is not suspended
+// - status passes visibility checks
+// - status passes interaction policy checks
+//
+// If status is not permitted to be stored, the function
+// will clean up after itself by removing the status.
+//
+// If status is a reply or a boost, and the author of
+// the given status is only permitted to reply or boost
+// pending approval, then "PendingApproval" will be set
+// to "true" on status. Callers should check this
+// and handle it as appropriate.
+func (d *Dereferencer) isPermittedStatus(
+ ctx context.Context,
+ requestUser string,
+ existing *gtsmodel.Status,
+ status *gtsmodel.Status,
+) (
+ bool, // is permitted?
+ error,
+) {
+ // our failure condition handling
+ // at the end of this function for
+ // the case of permission = false.
+ onFalse := func() (bool, error) {
+ if existing != nil {
+ log.Infof(ctx, "deleting unpermitted: %s", existing.URI)
+
+ // Delete existing status from database as it's no longer permitted.
+ if err := d.state.DB.DeleteStatusByID(ctx, existing.ID); err != nil {
+ log.Errorf(ctx, "error deleting %s after permissivity fail: %v", existing.URI, err)
+ }
+ }
+ return false, nil
+ }
+
+ if status.Account.IsSuspended() {
+ // The status author is suspended,
+ // this shouldn't have reached here
+ // but it's a fast check anyways.
+ log.Debugf(ctx,
+ "status author %s is suspended",
+ status.AccountURI,
+ )
+ return onFalse()
+ }
+
+ if inReplyTo := status.InReplyTo; inReplyTo != nil {
+ return d.isPermittedReply(
+ ctx,
+ requestUser,
+ status,
+ inReplyTo,
+ onFalse,
+ )
+ } else if boostOf := status.BoostOf; boostOf != nil {
+ return d.isPermittedBoost(
+ ctx,
+ requestUser,
+ status,
+ boostOf,
+ onFalse,
+ )
+ }
+
+ // Nothing else stopping this.
+ return true, nil
+}
+
+func (d *Dereferencer) isPermittedReply(
+ ctx context.Context,
+ requestUser string,
+ status *gtsmodel.Status,
+ inReplyTo *gtsmodel.Status,
+ onFalse func() (bool, error),
+) (bool, error) {
+ if inReplyTo.BoostOfID != "" {
+ // We do not permit replies to
+ // boost wrapper statuses. (this
+ // shouldn't be able to happen).
+ log.Info(ctx, "rejecting reply to boost wrapper status")
+ return onFalse()
+ }
+
+ // Check visibility of local
+ // inReplyTo to replying account.
+ if inReplyTo.IsLocal() {
+ visible, err := d.visFilter.StatusVisible(ctx,
+ status.Account,
+ inReplyTo,
+ )
+ if err != nil {
+ err := gtserror.Newf("error checking inReplyTo visibility: %w", err)
+ return false, err
+ }
+
+ // Our status is not visible to the
+ // account trying to do the reply.
+ if !visible {
+ return onFalse()
+ }
+ }
+
+ // Check interaction policy of inReplyTo.
+ replyable, err := d.intFilter.StatusReplyable(ctx,
+ status.Account,
+ inReplyTo,
+ )
+ if err != nil {
+ err := gtserror.Newf("error checking status replyability: %w", err)
+ return false, err
+ }
+
+ if replyable.Forbidden() {
+ // Replier is not permitted
+ // to do this interaction.
+ return onFalse()
+ }
+
+ // TODO in next PR: check conditional /
+ // with approval and deref Accept.
+ if !replyable.Permitted() {
+ return onFalse()
+ }
+
+ return true, nil
+}
+
+func (d *Dereferencer) isPermittedBoost(
+ ctx context.Context,
+ requestUser string,
+ status *gtsmodel.Status,
+ boostOf *gtsmodel.Status,
+ onFalse func() (bool, error),
+) (bool, error) {
+ if boostOf.BoostOfID != "" {
+ // We do not permit boosts of
+ // boost wrapper statuses. (this
+ // shouldn't be able to happen).
+ log.Info(ctx, "rejecting boost of boost wrapper status")
+ return onFalse()
+ }
+
+ // Check visibility of local
+ // boostOf to boosting account.
+ if boostOf.IsLocal() {
+ visible, err := d.visFilter.StatusVisible(ctx,
+ status.Account,
+ boostOf,
+ )
+ if err != nil {
+ err := gtserror.Newf("error checking boostOf visibility: %w", err)
+ return false, err
+ }
+
+ // Our status is not visible to the
+ // account trying to do the boost.
+ if !visible {
+ return onFalse()
+ }
+ }
+
+ // Check interaction policy of boostOf.
+ boostable, err := d.intFilter.StatusBoostable(ctx,
+ status.Account,
+ boostOf,
+ )
+ if err != nil {
+ err := gtserror.Newf("error checking status boostability: %w", err)
+ return false, err
+ }
+
+ if boostable.Forbidden() {
+ // Booster is not permitted
+ // to do this interaction.
+ return onFalse()
+ }
+
+ // TODO in next PR: check conditional /
+ // with approval and deref Accept.
+ if !boostable.Permitted() {
+ return onFalse()
+ }
+
+ return true, nil
+}