[chore] Refactor AP authentication, other small bits of tidying up (#1874)

This commit is contained in:
tobi
2023-06-13 16:47:56 +02:00
committed by GitHub
parent 433b56d2f9
commit 24fbdf2b0a
38 changed files with 1280 additions and 996 deletions

View File

@ -20,23 +20,56 @@ package federation
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"codeberg.org/gruf/go-kv"
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
type errOtherIRIBlocked struct {
account string
domainBlock bool
iriStrs []string
}
func (e errOtherIRIBlocked) Error() string {
iriStrsNice := "[" + strings.Join(e.iriStrs, ", ") + "]"
if e.domainBlock {
return "domain block exists for one or more of " + iriStrsNice
}
return "block exists between " + e.account + " and one or more of " + iriStrsNice
}
func newErrOtherIRIBlocked(
account string,
domainBlock bool,
otherIRIs []*url.URL,
) error {
e := errOtherIRIBlocked{
account: account,
domainBlock: domainBlock,
iriStrs: make([]string, 0, len(otherIRIs)),
}
for _, iri := range otherIRIs {
e.iriStrs = append(e.iriStrs, iri.String())
}
return e
}
/*
GO FED FEDERATING PROTOCOL INTERFACE
FederatingProtocol contains behaviors an application needs to satisfy for the
@ -47,77 +80,104 @@ import (
application.
*/
// PostInboxRequestBodyHook callback after parsing the request body for a federated request
// to the Actor's inbox.
// PostInboxRequestBodyHook callback after parsing the request body for a
// federated request to the Actor's inbox.
//
// Can be used to set contextual information based on the Activity
// received.
//
// Only called if the Federated Protocol is enabled.
// Can be used to set contextual information based on the Activity received.
//
// Warning: Neither authentication nor authorization has taken place at
// this time. Doing anything beyond setting contextual information is
// strongly discouraged.
//
// If an error is returned, it is passed back to the caller of
// PostInbox. In this case, the DelegateActor implementation must not
// write a response to the ResponseWriter as is expected that the caller
// to PostInbox will do so when handling the error.
// If an error is returned, it is passed back to the caller of PostInbox.
// In this case, the DelegateActor implementation must not write a response
// to the ResponseWriter as is expected that the caller to PostInbox will
// do so when handling the error.
func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
// extract any other IRIs involved in this activity
otherInvolvedIRIs := []*url.URL{}
// Extract any other IRIs involved in this activity.
otherIRIs := []*url.URL{}
// check if the Activity itself has an 'inReplyTo'
// Get the ID of the Activity itslf.
activityID, err := pub.GetId(activity)
if err == nil {
otherIRIs = append(otherIRIs, activityID)
}
// Check if the Activity has an 'inReplyTo'.
if replyToable, ok := activity.(ap.ReplyToable); ok {
if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
otherInvolvedIRIs = append(otherInvolvedIRIs, inReplyToURI)
otherIRIs = append(otherIRIs, inReplyToURI)
}
}
// now check if the Object of the Activity (usually a Note or something) has an 'inReplyTo'
if object := activity.GetActivityStreamsObject(); object != nil {
if replyToable, ok := object.(ap.ReplyToable); ok {
if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
otherInvolvedIRIs = append(otherInvolvedIRIs, inReplyToURI)
}
}
}
// check for Tos and CCs on Activity itself
// Check for TOs and CCs on the Activity.
if addressable, ok := activity.(ap.Addressable); ok {
if ccURIs, err := ap.ExtractCCs(addressable); err == nil {
otherInvolvedIRIs = append(otherInvolvedIRIs, ccURIs...)
}
if toURIs, err := ap.ExtractTos(addressable); err == nil {
otherInvolvedIRIs = append(otherInvolvedIRIs, toURIs...)
otherIRIs = append(otherIRIs, toURIs...)
}
if ccURIs, err := ap.ExtractCCs(addressable); err == nil {
otherIRIs = append(otherIRIs, ccURIs...)
}
}
// and on the Object itself
if object := activity.GetActivityStreamsObject(); object != nil {
if addressable, ok := object.(ap.Addressable); ok {
if ccURIs, err := ap.ExtractCCs(addressable); err == nil {
otherInvolvedIRIs = append(otherInvolvedIRIs, ccURIs...)
// Now perform the same checks, but for the Object(s) of the Activity.
objectProp := activity.GetActivityStreamsObject()
for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
if iter.IsIRI() {
otherIRIs = append(otherIRIs, iter.GetIRI())
continue
}
t := iter.GetType()
if t == nil {
continue
}
objectID, err := pub.GetId(t)
if err == nil {
otherIRIs = append(otherIRIs, objectID)
}
if replyToable, ok := t.(ap.ReplyToable); ok {
if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
otherIRIs = append(otherIRIs, inReplyToURI)
}
}
if addressable, ok := t.(ap.Addressable); ok {
if toURIs, err := ap.ExtractTos(addressable); err == nil {
otherInvolvedIRIs = append(otherInvolvedIRIs, toURIs...)
otherIRIs = append(otherIRIs, toURIs...)
}
if ccURIs, err := ap.ExtractCCs(addressable); err == nil {
otherIRIs = append(otherIRIs, ccURIs...)
}
}
}
// remove any duplicate entries in the slice we put together
deduped := util.UniqueURIs(otherInvolvedIRIs)
// Clean any instances of the public URI, since
// we don't care about that in this context.
otherIRIs = func(iris []*url.URL) []*url.URL {
np := make([]*url.URL, 0, len(iris))
// clean any instances of the public URI since we don't care about that in this context
cleaned := []*url.URL{}
for _, u := range deduped {
if !pub.IsPublic(u.String()) {
cleaned = append(cleaned, u)
for _, i := range iris {
if !pub.IsPublic(i.String()) {
np = append(np, i)
}
}
}
withOtherInvolvedIRIs := context.WithValue(ctx, ap.ContextOtherInvolvedIRIs, cleaned)
return withOtherInvolvedIRIs, nil
return np
}(otherIRIs)
// OtherIRIs will likely contain some
// duplicate entries now, so remove them.
otherIRIs = util.UniqueURIs(otherIRIs)
// Finished, set other IRIs on the context
// so they can be checked for blocks later.
ctx = gtscontext.SetOtherIRIs(ctx, otherIRIs)
return ctx, nil
}
// AuthenticatePostInbox delegates the authentication of a POST to an
@ -143,23 +203,23 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
// account by parsing username from `/users/{username}/inbox`.
username, err := uris.ParseInboxPath(r.URL)
if err != nil {
err = fmt.Errorf("AuthenticatePostInbox: could not parse %s as inbox path: %w", r.URL.String(), err)
err = gtserror.Newf("could not parse %s as inbox path: %w", r.URL.String(), err)
return nil, false, err
}
if username == "" {
err = errors.New("AuthenticatePostInbox: inbox username was empty")
err = gtserror.New("inbox username was empty")
return nil, false, err
}
receivingAccount, err := f.db.GetAccountByUsernameDomain(ctx, username, "")
if err != nil {
err = fmt.Errorf("AuthenticatePostInbox: could not fetch receiving account %s: %w", username, err)
err = gtserror.Newf("could not fetch receiving account %s: %w", username, err)
return nil, false, err
}
// Check who's delivering by inspecting the http signature.
publicKeyOwnerURI, errWithCode := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username)
// Check who's trying to deliver to us by inspecting the http signature.
pubKeyOwner, errWithCode := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username)
if errWithCode != nil {
switch errWithCode.Code() {
case http.StatusUnauthorized, http.StatusForbidden, http.StatusBadRequest:
@ -184,25 +244,30 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
// Authentication has passed, check if we need to create a
// new instance entry for the Host of the requesting account.
if _, err := f.db.GetInstance(ctx, publicKeyOwnerURI.Host); err != nil {
if _, err := f.db.GetInstance(ctx, pubKeyOwner.Host); err != nil {
if !errors.Is(err, db.ErrNoEntries) {
// There's been an actual error.
err = fmt.Errorf("AuthenticatePostInbox: error getting instance %s: %w", publicKeyOwnerURI.Host, err)
err = gtserror.Newf("error getting instance %s: %w", pubKeyOwner.Host, err)
return ctx, false, err
}
// we don't have an entry for this instance yet so dereference it
instance, err := f.GetRemoteInstance(gtscontext.SetFastFail(ctx), username, &url.URL{
Scheme: publicKeyOwnerURI.Scheme,
Host: publicKeyOwnerURI.Host,
})
// We don't have an entry for this
// instance yet; go dereference it.
instance, err := f.GetRemoteInstance(
gtscontext.SetFastFail(ctx),
username,
&url.URL{
Scheme: pubKeyOwner.Scheme,
Host: pubKeyOwner.Host,
},
)
if err != nil {
err = fmt.Errorf("AuthenticatePostInbox: error dereferencing instance %s: %w", publicKeyOwnerURI.Host, err)
err = gtserror.Newf("error dereferencing instance %s: %w", pubKeyOwner.Host, err)
return nil, false, err
}
if err := f.db.Put(ctx, instance); err != nil {
err = fmt.Errorf("AuthenticatePostInbox: error inserting instance entry for %s: %w", publicKeyOwnerURI.Host, err)
if err := f.db.Put(ctx, instance); err != nil && !errors.Is(err, db.ErrAlreadyExists) {
err = gtserror.Newf("error inserting instance entry for %s: %w", pubKeyOwner.Host, err)
return nil, false, err
}
}
@ -210,7 +275,11 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
// We know the public key owner URI now, so we can
// dereference the remote account (or just get it
// from the db if we already have it).
requestingAccount, _, err := f.GetAccountByURI(gtscontext.SetFastFail(ctx), username, publicKeyOwnerURI)
requestingAccount, _, err := f.GetAccountByURI(
gtscontext.SetFastFail(ctx),
username,
pubKeyOwner,
)
if err != nil {
if gtserror.StatusCode(err) == http.StatusGone {
// This is the same case as the http.StatusGone check above.
@ -222,113 +291,196 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
w.WriteHeader(http.StatusAccepted)
return ctx, false, nil
}
err = fmt.Errorf("AuthenticatePostInbox: couldn't get requesting account %s: %w", publicKeyOwnerURI, err)
err = gtserror.Newf("couldn't get requesting account %s: %w", pubKeyOwner, err)
return nil, false, err
}
// We have everything we need now, set the requesting
// and receiving accounts on the context for later use.
withRequesting := context.WithValue(ctx, ap.ContextRequestingAccount, requestingAccount)
withReceiving := context.WithValue(withRequesting, ap.ContextReceivingAccount, receivingAccount)
return withReceiving, true, nil
ctx = gtscontext.SetRequestingAccount(ctx, requestingAccount)
ctx = gtscontext.SetReceivingAccount(ctx, receivingAccount)
return ctx, true, nil
}
// Blocked should determine whether to permit a set of actors given by
// their ids are able to interact with this particular end user due to
// being blocked or other application-specific logic.
//
// If an error is returned, it is passed back to the caller of
// PostInbox.
//
// If no error is returned, but authentication or authorization fails,
// then blocked must be true and error nil. An http.StatusForbidden
// will be written in the wresponse.
//
// Finally, if the authentication and authorization succeeds, then
// blocked must be false and error nil. The request will continue
// to be processed.
func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {
log.Tracef(ctx, "entering BLOCKED function with IRI list: %+v", actorIRIs)
// Fetch relevant items from request context.
// These should have been set further up the flow.
receivingAccount := gtscontext.ReceivingAccount(ctx)
if receivingAccount == nil {
err := gtserror.New("couldn't determine blocks (receiving account not set on request context)")
return false, err
}
// check domain blocks first for the given actor IRIs
requestingAccount := gtscontext.RequestingAccount(ctx)
if requestingAccount == nil {
err := gtserror.New("couldn't determine blocks (requesting account not set on request context)")
return false, err
}
otherIRIs := gtscontext.OtherIRIs(ctx)
if otherIRIs == nil {
err := gtserror.New("couldn't determine blocks (otherIRIs not set on request context)")
return false, err
}
l := log.
WithContext(ctx).
WithFields(kv.Fields{
{"actorIRIs", actorIRIs},
{"receivingAccount", receivingAccount.URI},
{"requestingAccount", requestingAccount.URI},
{"otherIRIs", otherIRIs},
}...)
l.Trace("checking blocks")
// Start broad by checking domain-level blocks first for
// the given actor IRIs; if any of them are domain blocked
// then we can save some work.
blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs)
if err != nil {
return false, fmt.Errorf("error checking domain blocks of actorIRIs: %s", err)
err = gtserror.Newf("error checking domain blocks of actorIRIs: %w", err)
return false, err
}
if blocked {
l.Trace("one or more actorIRIs are domain blocked")
return blocked, nil
}
// check domain blocks for any other involved IRIs
otherInvolvedIRIsI := ctx.Value(ap.ContextOtherInvolvedIRIs)
otherInvolvedIRIs, ok := otherInvolvedIRIsI.([]*url.URL)
if !ok {
log.Error(ctx, "other involved IRIs not set on request context")
return false, errors.New("other involved IRIs not set on request context, so couldn't determine blocks")
}
blocked, err = f.db.AreURIsBlocked(ctx, otherInvolvedIRIs)
if err != nil {
return false, fmt.Errorf("error checking domain blocks of otherInvolvedIRIs: %s", err)
}
if blocked {
return blocked, nil
}
// now check for user-level block from receiving against requesting account
receivingAccountI := ctx.Value(ap.ContextReceivingAccount)
receivingAccount, ok := receivingAccountI.(*gtsmodel.Account)
if !ok {
log.Error(ctx, "receiving account not set on request context")
return false, errors.New("receiving account not set on request context, so couldn't determine blocks")
}
requestingAccountI := ctx.Value(ap.ContextRequestingAccount)
requestingAccount, ok := requestingAccountI.(*gtsmodel.Account)
if !ok {
log.Error(ctx, "requesting account not set on request context")
return false, errors.New("requesting account not set on request context, so couldn't determine blocks")
}
// the receiver shouldn't block the sender
// Now user level blocks. Receiver should not block requester.
blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, requestingAccount.ID)
if err != nil {
return false, fmt.Errorf("error checking user-level blocks: %s", err)
err = gtserror.Newf("db error checking block between receiver and requester: %w", err)
return false, err
}
if blocked {
l.Trace("receiving account blocks requesting account")
return blocked, nil
}
// get account IDs for other involved accounts
var involvedAccountIDs []string
for _, iri := range otherInvolvedIRIs {
var involvedAccountID string
if involvedStatus, err := f.db.GetStatusByURI(ctx, iri.String()); err == nil {
involvedAccountID = involvedStatus.AccountID
} else if involvedAccount, err := f.db.GetAccountByURI(ctx, iri.String()); err == nil {
involvedAccountID = involvedAccount.ID
// We've established that no blocks exist between directly
// involved actors, but what about IRIs of other actors and
// objects which are tangentially involved in the activity
// (ie., replied to, boosted)?
//
// If one or more of these other IRIs is domain blocked, or
// blocked by the receiving account, this shouldn't return
// blocked=true to send a 403, since that would be rather
// silly behavior. Instead, we should indicate to the caller
// that we should stop processing the activity and just write
// 202 Accepted instead.
//
// For this, we can use the errOtherIRIBlocked type, which
// will be checked for
// Check high-level domain blocks first.
blocked, err = f.db.AreURIsBlocked(ctx, otherIRIs)
if err != nil {
err := gtserror.Newf("error checking domain block of otherIRIs: %w", err)
return false, err
}
if blocked {
err := newErrOtherIRIBlocked(receivingAccount.URI, true, otherIRIs)
l.Trace(err.Error())
return false, err
}
// For each other IRI, check whether the IRI points to an
// account or a status, and try to get (an) accountID(s)
// from it to do further checks on.
//
// We use a map for this instead of a slice in order to
// deduplicate entries and avoid doing the same check twice.
// The map value is the host of the otherIRI.
accountIDs := make(map[string]string, len(otherIRIs))
for _, iri := range otherIRIs {
// Assemble iri string just once.
iriStr := iri.String()
account, err := f.db.GetAccountByURI(
// We're on a hot path, fetch bare minimum.
gtscontext.SetBarebones(ctx),
iriStr,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// Real db error.
err = gtserror.Newf("db error trying to get %s as account: %w", iriStr, err)
return false, err
} else if err == nil {
// IRI is for an account.
accountIDs[account.ID] = iri.Host
continue
}
if involvedAccountID != "" {
involvedAccountIDs = append(involvedAccountIDs, involvedAccountID)
status, err := f.db.GetStatusByURI(
// We're on a hot path, fetch bare minimum.
gtscontext.SetBarebones(ctx),
iriStr,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// Real db error.
err = gtserror.Newf("db error trying to get %s as status: %w", iriStr, err)
return false, err
} else if err == nil {
// IRI is for a status.
accountIDs[status.AccountID] = iri.Host
continue
}
}
deduped := util.UniqueStrings(involvedAccountIDs)
for _, involvedAccountID := range deduped {
// the involved account shouldn't block whoever is making this request
blocked, err = f.db.IsBlocked(ctx, involvedAccountID, requestingAccount.ID)
// Get our own host value just once outside the loop.
ourHost := config.GetHost()
for accountID, iriHost := range accountIDs {
// Receiver shouldn't block other IRI owner.
//
// This check protects against cases where someone on our
// instance is receiving a boost from someone they don't
// block, but the boost target is the status of an account
// they DO have blocked, or the boosted status mentions an
// account they have blocked. In this case, it's v. unlikely
// they care to see the boost in their timeline, so there's
// no point in us processing it.
blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, accountID)
if err != nil {
return false, fmt.Errorf("error checking user-level otherInvolvedIRI blocks: %s", err)
}
if blocked {
return blocked, nil
err = gtserror.Newf("db error checking block between receiver and other account: %w", err)
return false, err
}
// whoever is receiving this request shouldn't block the involved account
blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, involvedAccountID)
if err != nil {
return false, fmt.Errorf("error checking user-level otherInvolvedIRI blocks: %s", err)
}
if blocked {
return blocked, nil
l.Trace("receiving account blocks one or more otherIRIs")
err := newErrOtherIRIBlocked(receivingAccount.URI, false, otherIRIs)
return false, err
}
// If other account is from our instance (indicated by the
// host of the URI stored in the map), ensure they don't block
// the requester.
//
// This check protects against cases where one of our users
// might be mentioned by the requesting account, and therefore
// appear in otherIRIs, but the activity itself has been sent
// to a different account on our instance. In other words, two
// accounts are gossiping about + trying to tag a third account
// who has one or the other of them blocked.
if iriHost == ourHost {
blocked, err = f.db.IsBlocked(ctx, accountID, requestingAccount.ID)
if err != nil {
err = gtserror.Newf("db error checking block between other account and requester: %w", err)
return false, err
}
if blocked {
l.Trace("one or more otherIRIs belonging to us blocks requesting account")
err := newErrOtherIRIBlocked(requestingAccount.URI, false, otherIRIs)
return false, err
}
}
}