[chore] No sigs for retrieving instance actor

As a possible path towards resolving #1186, this removes the signature
check on the instance actor. Also adds a test to check authentication
for a user other than the instance actor to ensure we don't poke a big
hole into that check.
This commit is contained in:
Daenney 2025-06-03 19:11:07 +02:00
commit e65bda768c
6 changed files with 120 additions and 4 deletions

View file

@ -21,6 +21,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/api/activitypub/emoji"
"code.superseriousbusiness.org/gotosocial/internal/api/activitypub/publickey"
"code.superseriousbusiness.org/gotosocial/internal/api/activitypub/users"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/middleware"
"code.superseriousbusiness.org/gotosocial/internal/processing"
@ -40,14 +41,22 @@ func (a *ActivityPub) Route(r *router.Router, m ...gin.HandlerFunc) {
emojiGroup := r.AttachGroup("emoji")
usersGroup := r.AttachGroup("users")
emojiGroup.Use(m...)
usersGroup.Use(m...)
// attach shared, non-global middlewares to both of these groups
ccMiddleware := middleware.CacheControl(middleware.CacheControlConfig{
Directives: []string{"no-store"},
})
emojiGroup.Use(m...)
usersGroup.Use(m...)
emojiGroup.Use(a.signatureCheckMiddleware, ccMiddleware)
usersGroup.Use(a.signatureCheckMiddleware, ccMiddleware)
emojiGroup.Use(ccMiddleware, a.signatureCheckMiddleware)
usersGroup.Use(ccMiddleware)
// hook the instance actor route first so we don't require auth
usersGroup.GET(config.InstanceActor(), a.users.InstanceActorGETHandler)
// add signature checking to any other users routes
usersGroup.Use(a.signatureCheckMiddleware)
a.emoji.Route(emojiGroup.Handle)
a.users.Route(usersGroup.Handle)

View file

@ -23,6 +23,7 @@ import (
"strings"
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"github.com/gin-gonic/gin"
)
@ -65,3 +66,19 @@ func (m *Module) UsersGETHandler(c *gin.Context) {
apiutil.JSONType(c, http.StatusOK, contentType, resp)
}
func (m *Module) InstanceActorGETHandler(c *gin.Context) {
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubHeaders...)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
}
resp, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), config.InstanceActor(), c.Request.URL)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
apiutil.JSONType(c, http.StatusOK, contentType, resp)
}

View file

@ -19,6 +19,7 @@ package users_test
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -158,6 +159,70 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
suite.EqualValues(targetAccount.Username, a.Username)
}
func (suite *UserGetTestSuite) TestGetUserAuth() {
targetAccount := suite.testAccounts["local_account_1"]
// setup request
recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.URI, nil) // the endpoint we're hitting
ctx.Request.Header.Set("accept", "application/activity+json")
// we need to pass the context through signature check first to set appropriate values on it
suite.signatureCheck(ctx)
// normally the router would populate these params from the path values,
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
Key: users.UsernameKey,
Value: targetAccount.Username,
},
}
// trigger the function being tested
suite.userModule.UsersGETHandler(ctx)
// check response
suite.EqualValues(http.StatusUnauthorized, recorder.Code)
}
func (suite *UserGetTestSuite) TestInstanceActor() {
targetAccount := suite.testAccounts["instance_account"]
// setup request
recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.URI, nil) // the endpoint we're hitting
ctx.Request.Header.Set("accept", "application/activity+json")
// trigger the function being tested
suite.userModule.InstanceActorGETHandler(ctx)
// check response
suite.EqualValues(http.StatusOK, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := io.ReadAll(result.Body)
suite.NoError(err)
// should be a Service
m := make(map[string]interface{})
err = json.Unmarshal(b, &m)
suite.NoError(err)
t, err := streams.ToType(suite.T().Context(), m)
suite.NoError(err)
// ensure it's a Service
svc, ok := t.(vocab.ActivityStreamsService)
suite.True(ok)
suite.Equal(targetAccount.Username, svc.GetActivityStreamsPreferredUsername().GetIRI().String())
}
func TestUserGetTestSuite(t *testing.T) {
suite.Run(t, new(UserGetTestSuite))
}

View file

@ -72,3 +72,12 @@ func mapGet(m map[string]any, keys ...string) (any, bool) {
}
return nil, false
}
func InstanceActor() string {
a := GetHost()
if a == "" {
a = GetAccountDomain()
}
return a
}

View file

@ -24,6 +24,7 @@ import (
"net/url"
"code.superseriousbusiness.org/gotosocial/internal/ap"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/uris"
@ -46,6 +47,15 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque
return nil, gtserror.NewErrorInternalError(err)
}
if requestedUsername == config.InstanceActor() && uris.IsInstanceActorPath(requestURL) {
accountable, err := p.converter.AccountToAS(ctx, receiver)
if err != nil {
err := gtserror.Newf("error converting account: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return data(accountable)
}
if uris.IsPublicKeyPath(requestURL) {
// If request is on a public key path, we don't need to
// authenticate this request. However, we'll only serve

View file

@ -20,6 +20,7 @@ package uris
import (
"fmt"
"net/url"
"path"
"strings"
"code.superseriousbusiness.org/gotosocial/internal/config"
@ -325,6 +326,11 @@ func IsPublicKeyPath(id *url.URL) bool {
return regexes.PublicKeyPath.MatchString(id.Path)
}
// IsInstanceActorPath returns true if the given URL corresponds to /users/instance_actor
func IsInstanceActorPath(u *url.URL) bool {
return u.Path == path.Join("/", "users", config.InstanceActor())
}
// IsBlockPath returns true if the given URL path corresponds to eg /users/example_username/blocks/SOME_ULID_OF_A_BLOCK
func IsBlockPath(id *url.URL) bool {
return regexes.BlockPath.MatchString(id.Path)