activitypub: Implement an instance-wide actor
An instance-wide actor is required for outgoing signed requests that are done on behalf of the instance, rather than on behalf of other actors. Such things include updating profile information, or fetching public keys. Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
This commit is contained in:
parent
cd17eb0fa7
commit
f121e87aa6
|
@ -4,8 +4,10 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -68,3 +70,28 @@ func NewActionsUser() *User {
|
||||||
func (u *User) IsActions() bool {
|
func (u *User) IsActions() bool {
|
||||||
return u != nil && u.ID == ActionsUserID
|
return u != nil && u.ID == ActionsUserID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
APActorUserID = -3
|
||||||
|
APActorUserName = "actor"
|
||||||
|
APActorEmail = "noreply@forgejo.org"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAPActorUser() *User {
|
||||||
|
return &User{
|
||||||
|
ID: APActorUserID,
|
||||||
|
Name: APActorUserName,
|
||||||
|
LowerName: APActorUserName,
|
||||||
|
IsActive: true,
|
||||||
|
Email: APActorEmail,
|
||||||
|
KeepEmailPrivate: true,
|
||||||
|
LoginName: APActorUserName,
|
||||||
|
Type: UserTypeIndividual,
|
||||||
|
Visibility: structs.VisibleTypePublic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func APActorUserAPActorID() string {
|
||||||
|
path, _ := url.JoinPath(setting.AppURL, "/api/v1/activitypub/actor")
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package activitypub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/activitypub"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Actor function returns the instance's Actor
|
||||||
|
func Actor(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /activitypub/actor activitypub activitypubInstanceActor
|
||||||
|
// ---
|
||||||
|
// summary: Returns the instance's Actor
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/ActivityPub"
|
||||||
|
|
||||||
|
link := user_model.APActorUserAPActorID()
|
||||||
|
actor := ap.ActorNew(ap.IRI(link), ap.ApplicationType)
|
||||||
|
|
||||||
|
actor.PreferredUsername = ap.NaturalLanguageValuesNew()
|
||||||
|
err := actor.PreferredUsername.Set("en", ap.Content(setting.Domain))
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("PreferredUsername.Set", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actor.URL = ap.IRI(setting.AppURL)
|
||||||
|
|
||||||
|
actor.Inbox = ap.IRI(link + "/inbox")
|
||||||
|
actor.Outbox = ap.IRI(link + "/outbox")
|
||||||
|
|
||||||
|
actor.PublicKey.ID = ap.IRI(link + "#main-key")
|
||||||
|
actor.PublicKey.Owner = ap.IRI(link)
|
||||||
|
|
||||||
|
publicKeyPem, err := activitypub.GetPublicKey(ctx, user_model.NewAPActorUser())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetPublicKey", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actor.PublicKey.PublicKeyPem = publicKeyPem
|
||||||
|
|
||||||
|
binary, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(ap.ActivityBaseURI),
|
||||||
|
jsonld.IRI(ap.SecurityContextURI),
|
||||||
|
).Marshal(actor)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("MarshalJSON", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType)
|
||||||
|
ctx.Resp.WriteHeader(http.StatusOK)
|
||||||
|
if _, err = ctx.Resp.Write(binary); err != nil {
|
||||||
|
log.Error("write to resp err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActorInbox function handles the incoming data for the instance Actor
|
||||||
|
func ActorInbox(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /activitypub/actor/inbox activitypub activitypubInstanceActorInbox
|
||||||
|
// ---
|
||||||
|
// summary: Send to the inbox
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
|
@ -805,6 +805,10 @@ func Routes() *web.Route {
|
||||||
m.Get("", activitypub.Person)
|
m.Get("", activitypub.Person)
|
||||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||||
}, context.UserIDAssignmentAPI())
|
}, context.UserIDAssignmentAPI())
|
||||||
|
m.Group("/actor", func() {
|
||||||
|
m.Get("", activitypub.Actor)
|
||||||
|
m.Post("/inbox", activitypub.ActorInbox)
|
||||||
|
})
|
||||||
m.Group("/repository-id/{repository-id}", func() {
|
m.Group("/repository-id/{repository-id}", func() {
|
||||||
m.Get("", activitypub.Repository)
|
m.Get("", activitypub.Repository)
|
||||||
m.Post("/inbox",
|
m.Post("/inbox",
|
||||||
|
|
|
@ -23,6 +23,40 @@
|
||||||
},
|
},
|
||||||
"basePath": "{{AppSubUrl | JSEscape}}/api/v1",
|
"basePath": "{{AppSubUrl | JSEscape}}/api/v1",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/activitypub/actor": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"activitypub"
|
||||||
|
],
|
||||||
|
"summary": "Returns the instance's Actor",
|
||||||
|
"operationId": "activitypubInstanceActor",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/ActivityPub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/activitypub/actor/inbox": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"activitypub"
|
||||||
|
],
|
||||||
|
"summary": "Send to the inbox",
|
||||||
|
"operationId": "activitypubInstanceActorInbox",
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"$ref": "#/responses/empty"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/activitypub/repository-id/{repository-id}": {
|
"/activitypub/repository-id/{repository-id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/routers"
|
||||||
|
|
||||||
|
ap "github.com/go-ap/activitypub"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActivityPubActor(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/activitypub/actor")
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
body := resp.Body.Bytes()
|
||||||
|
assert.Contains(t, string(body), "@context")
|
||||||
|
|
||||||
|
var actor ap.Actor
|
||||||
|
err := actor.UnmarshalJSON(body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ap.ApplicationType, actor.Type)
|
||||||
|
assert.Equal(t, setting.Domain, actor.PreferredUsername.String())
|
||||||
|
keyID := actor.GetID().String()
|
||||||
|
assert.Regexp(t, "activitypub/actor$", keyID)
|
||||||
|
assert.Regexp(t, "activitypub/actor/outbox$", actor.Outbox.GetID().String())
|
||||||
|
assert.Regexp(t, "activitypub/actor/inbox$", actor.Inbox.GetID().String())
|
||||||
|
|
||||||
|
pubKey := actor.PublicKey
|
||||||
|
assert.NotNil(t, pubKey)
|
||||||
|
publicKeyID := keyID + "#main-key"
|
||||||
|
assert.Equal(t, pubKey.ID.String(), publicKeyID)
|
||||||
|
|
||||||
|
pubKeyPem := pubKey.PublicKeyPem
|
||||||
|
assert.NotNil(t, pubKeyPem)
|
||||||
|
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue