diff options
Diffstat (limited to 'internal/federation/dereferencing')
-rw-r--r-- | internal/federation/dereferencing/announce.go | 24 | ||||
-rw-r--r-- | internal/federation/dereferencing/dereferencer.go | 11 | ||||
-rw-r--r-- | internal/federation/dereferencing/dereferencer_test.go | 14 | ||||
-rw-r--r-- | internal/federation/dereferencing/status.go | 83 | ||||
-rw-r--r-- | internal/federation/dereferencing/status_permitted.go | 216 |
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 +} |