feat(activitiypub): enable HTTP signatures on all ActivityPub endpoints (#7035)
- Set the right keyID and use the right signing keys for outgoing requests. - Verify the HTTP signature of all incoming requests, except for the server actor. - Caches keys of incoming requests for users and servers actors. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7035 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: famfo <famfo@famfo.xyz> Co-committed-by: famfo <famfo@famfo.xyz>
This commit is contained in:
parent
ba5b157f7e
commit
77b0275572
22 changed files with 681 additions and 122 deletions
|
@ -4,6 +4,7 @@
|
||||||
package forgefed
|
package forgefed
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -15,12 +16,14 @@ import (
|
||||||
// FederationHost data type
|
// FederationHost data type
|
||||||
// swagger:model
|
// swagger:model
|
||||||
type FederationHost struct {
|
type FederationHost struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
||||||
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
||||||
LatestActivity time.Time `xorm:"NOT NULL"`
|
LatestActivity time.Time `xorm:"NOT NULL"`
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
||||||
|
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factory function for FederationHost. Created struct is asserted to be valid.
|
// Factory function for FederationHost. Created struct is asserted to be valid.
|
||||||
|
|
|
@ -30,9 +30,9 @@ func GetFederationHost(ctx context.Context, ID int64) (*FederationHost, error) {
|
||||||
return host, nil
|
return host, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindFederationHostByFqdn(ctx context.Context, fqdn string) (*FederationHost, error) {
|
func findFederationHostFromDB(ctx context.Context, searchKey, searchValue string) (*FederationHost, error) {
|
||||||
host := new(FederationHost)
|
host := new(FederationHost)
|
||||||
has, err := db.GetEngine(ctx).Where("host_fqdn=?", strings.ToLower(fqdn)).Get(host)
|
has, err := db.GetEngine(ctx).Where(searchKey, searchValue).Get(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
|
@ -44,6 +44,14 @@ func FindFederationHostByFqdn(ctx context.Context, fqdn string) (*FederationHost
|
||||||
return host, nil
|
return host, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindFederationHostByFqdn(ctx context.Context, fqdn string) (*FederationHost, error) {
|
||||||
|
return findFederationHostFromDB(ctx, "host_fqdn=?", strings.ToLower(fqdn))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindFederationHostByKeyID(ctx context.Context, keyID string) (*FederationHost, error) {
|
||||||
|
return findFederationHostFromDB(ctx, "key_id=?", keyID)
|
||||||
|
}
|
||||||
|
|
||||||
func CreateFederationHost(ctx context.Context, host *FederationHost) error {
|
func CreateFederationHost(ctx context.Context, host *FederationHost) error {
|
||||||
if res, err := validation.IsValid(host); !res {
|
if res, err := validation.IsValid(host); !res {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -94,6 +94,8 @@ var migrations = []*Migration{
|
||||||
NewMigration("Add `created_unix` column to `user_redirect` table", AddCreatedUnixToRedirect),
|
NewMigration("Add `created_unix` column to `user_redirect` table", AddCreatedUnixToRedirect),
|
||||||
// v27 -> v28
|
// v27 -> v28
|
||||||
NewMigration("Add pronoun privacy settings to user", AddHidePronounsOptionToUser),
|
NewMigration("Add pronoun privacy settings to user", AddHidePronounsOptionToUser),
|
||||||
|
// v28 -> v29
|
||||||
|
NewMigration("Add public key information to `FederatedUser` and `FederationHost`", AddPublicKeyInformationForFederation),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
29
models/forgejo_migrations/v29.go
Normal file
29
models/forgejo_migrations/v29.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package forgejo_migrations //nolint:revive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddPublicKeyInformationForFederation(x *xorm.Engine) error {
|
||||||
|
type FederationHost struct {
|
||||||
|
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
||||||
|
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := x.Sync(&FederationHost{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type FederatedUser struct {
|
||||||
|
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
||||||
|
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(&FederatedUser{})
|
||||||
|
}
|
44
models/user/activitypub.go
Normal file
44
models/user/activitypub.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"forgejo.org/models/db"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APActorID returns the IRI to the api endpoint of the user
|
||||||
|
func (u *User) APActorID() string {
|
||||||
|
if u.IsAPServerActor() {
|
||||||
|
return fmt.Sprintf("%sapi/v1/activitypub/actor", setting.AppURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%sapi/v1/activitypub/user-id/%s", setting.AppURL, url.PathEscape(fmt.Sprintf("%d", u.ID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// APActorKeyID returns the ID of the user's public key
|
||||||
|
func (u *User) APActorKeyID() string {
|
||||||
|
return u.APActorID() + "#main-key"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserByFederatedURI(ctx context.Context, federatedURI string) (*User, error) {
|
||||||
|
user := new(User)
|
||||||
|
has, err := db.GetEngine(ctx).Where("normalized_federated_uri=?", federatedURI).Get(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := validation.IsValid(*user); !res {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
|
@ -4,14 +4,20 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"forgejo.org/models/db"
|
||||||
"forgejo.org/modules/validation"
|
"forgejo.org/modules/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FederatedUser struct {
|
type FederatedUser struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"NOT NULL"`
|
UserID int64 `xorm:"NOT NULL"`
|
||||||
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
|
||||||
|
KeyID sql.NullString `xorm:"key_id UNIQUE"`
|
||||||
|
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFederatedUser(userID int64, externalID string, federationHostID int64) (FederatedUser, error) {
|
func NewFederatedUser(userID int64, externalID string, federationHostID int64) (FederatedUser, error) {
|
||||||
|
@ -26,6 +32,30 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64) (
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getFederatedUserFromDB(ctx context.Context, searchKey, searchValue any) (*FederatedUser, error) {
|
||||||
|
federatedUser := new(FederatedUser)
|
||||||
|
has, err := db.GetEngine(ctx).Where(searchKey, searchValue).Get(federatedUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := validation.IsValid(*federatedUser); !res {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return federatedUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFederatedUserByKeyID(ctx context.Context, keyID string) (*FederatedUser, error) {
|
||||||
|
return getFederatedUserFromDB(ctx, "key_id=?", keyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFederatedUserByUserID(ctx context.Context, userID int64) (*FederatedUser, error) {
|
||||||
|
return getFederatedUserFromDB(ctx, "user_id=?", userID)
|
||||||
|
}
|
||||||
|
|
||||||
func (user FederatedUser) Validate() []string {
|
func (user FederatedUser) Validate() []string {
|
||||||
var result []string
|
var result []string
|
||||||
result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
|
result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
|
||||||
|
|
|
@ -311,11 +311,6 @@ func (u *User) HTMLURL() string {
|
||||||
return setting.AppURL + url.PathEscape(u.Name)
|
return setting.AppURL + url.PathEscape(u.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// APActorID returns the IRI to the api endpoint of the user
|
|
||||||
func (u *User) APActorID() string {
|
|
||||||
return fmt.Sprintf("%vapi/v1/activitypub/user-id/%v", setting.AppURL, url.PathEscape(fmt.Sprintf("%v", u.ID)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrganisationLink returns the organization sub page link.
|
// OrganisationLink returns the organization sub page link.
|
||||||
func (u *User) OrganisationLink() string {
|
func (u *User) OrganisationLink() string {
|
||||||
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
||||||
|
|
|
@ -73,30 +73,30 @@ func (u *User) IsActions() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
APActorUserID = -3
|
APServerActorUserID = -3
|
||||||
APActorUserName = "actor"
|
APServerActorUserName = "actor"
|
||||||
APActorEmail = "noreply@forgejo.org"
|
APServerActorEmail = "noreply@forgejo.org"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAPActorUser() *User {
|
func NewAPServerActor() *User {
|
||||||
return &User{
|
return &User{
|
||||||
ID: APActorUserID,
|
ID: APServerActorUserID,
|
||||||
Name: APActorUserName,
|
Name: APServerActorUserName,
|
||||||
LowerName: APActorUserName,
|
LowerName: APServerActorUserName,
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
Email: APActorEmail,
|
Email: APServerActorEmail,
|
||||||
KeepEmailPrivate: true,
|
KeepEmailPrivate: true,
|
||||||
LoginName: APActorUserName,
|
LoginName: APServerActorUserName,
|
||||||
Type: UserTypeIndividual,
|
Type: UserTypeIndividual,
|
||||||
Visibility: structs.VisibleTypePublic,
|
Visibility: structs.VisibleTypePublic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func APActorUserAPActorID() string {
|
func APServerActorID() string {
|
||||||
path, _ := url.JoinPath(setting.AppURL, "/api/v1/activitypub/actor")
|
path, _ := url.JoinPath(setting.AppURL, "/api/v1/activitypub/actor")
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) IsAPActor() bool {
|
func (u *User) IsAPServerActor() bool {
|
||||||
return u != nil && u.ID == APActorUserID
|
return u != nil && u.ID == APServerActorUserID
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,9 +139,21 @@ func TestAPActorID(t *testing.T) {
|
||||||
user := user_model.User{ID: 1}
|
user := user_model.User{ID: 1}
|
||||||
url := user.APActorID()
|
url := user.APActorID()
|
||||||
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1"
|
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1"
|
||||||
if url != expected {
|
assert.Equal(t, expected, url)
|
||||||
t.Errorf("unexpected APActorID, expected: %q, actual: %q", expected, url)
|
}
|
||||||
}
|
|
||||||
|
func TestAPActorID_APActorID(t *testing.T) {
|
||||||
|
user := user_model.User{ID: user_model.APServerActorUserID}
|
||||||
|
url := user.APActorID()
|
||||||
|
expected := "https://try.gitea.io/api/v1/activitypub/actor"
|
||||||
|
assert.Equal(t, expected, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPActorKeyID(t *testing.T) {
|
||||||
|
user := user_model.User{ID: 1}
|
||||||
|
url := user.APActorKeyID()
|
||||||
|
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key"
|
||||||
|
assert.Equal(t, expected, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchUsers(t *testing.T) {
|
func TestSearchUsers(t *testing.T) {
|
||||||
|
|
|
@ -191,10 +191,17 @@ func (c *Client) GetBody(uri string) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
body, err := io.ReadAll(response.Body)
|
if response.ContentLength > setting.Federation.MaxSize {
|
||||||
|
return nil, fmt.Errorf("Request returned %d bytes (max allowed incomming size: %d bytes)", response.ContentLength, setting.Federation.MaxSize)
|
||||||
|
} else if response.ContentLength == -1 {
|
||||||
|
log.Warn("Request to %v returned an unknown content length, response may be truncated to %d bytes", uri, setting.Federation.MaxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(io.LimitReader(response.Body, setting.Federation.MaxSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Client: got body: %v", charLimiter(string(body), 120))
|
log.Debug("Client: got body: %v", charLimiter(string(body), 120))
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,18 +15,20 @@ var (
|
||||||
Enabled bool
|
Enabled bool
|
||||||
ShareUserStatistics bool
|
ShareUserStatistics bool
|
||||||
MaxSize int64
|
MaxSize int64
|
||||||
Algorithms []string
|
SignatureAlgorithms []string
|
||||||
DigestAlgorithm string
|
DigestAlgorithm string
|
||||||
GetHeaders []string
|
GetHeaders []string
|
||||||
PostHeaders []string
|
PostHeaders []string
|
||||||
|
SignatureEnforced bool
|
||||||
}{
|
}{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
ShareUserStatistics: true,
|
ShareUserStatistics: true,
|
||||||
MaxSize: 4,
|
MaxSize: 4,
|
||||||
Algorithms: []string{"rsa-sha256", "rsa-sha512", "ed25519"},
|
SignatureAlgorithms: []string{"rsa-sha256", "rsa-sha512", "ed25519"},
|
||||||
DigestAlgorithm: "SHA-256",
|
DigestAlgorithm: "SHA-256",
|
||||||
GetHeaders: []string{"(request-target)", "Date", "Host"},
|
GetHeaders: []string{"(request-target)", "Date", "Host"},
|
||||||
PostHeaders: []string{"(request-target)", "Date", "Host", "Digest"},
|
PostHeaders: []string{"(request-target)", "Date", "Host", "Digest"},
|
||||||
|
SignatureEnforced: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,8 +46,8 @@ func loadFederationFrom(rootCfg ConfigProvider) {
|
||||||
// Get MaxSize in bytes instead of MiB
|
// Get MaxSize in bytes instead of MiB
|
||||||
Federation.MaxSize = 1 << 20 * Federation.MaxSize
|
Federation.MaxSize = 1 << 20 * Federation.MaxSize
|
||||||
|
|
||||||
HttpsigAlgs = make([]httpsig.Algorithm, len(Federation.Algorithms))
|
HttpsigAlgs = make([]httpsig.Algorithm, len(Federation.SignatureAlgorithms))
|
||||||
for i, alg := range Federation.Algorithms {
|
for i, alg := range Federation.SignatureAlgorithms {
|
||||||
HttpsigAlgs[i] = httpsig.Algorithm(alg)
|
HttpsigAlgs[i] = httpsig.Algorithm(alg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, repository := range mock.Repositories {
|
for _, repository := range mock.Repositories {
|
||||||
federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/repository-id/%v/inbox/", repository.ID),
|
federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/repository-id/%v/inbox", repository.ID),
|
||||||
func(res http.ResponseWriter, req *http.Request) {
|
func(res http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != "POST" {
|
if req.Method != "POST" {
|
||||||
t.Errorf("POST expected at: %q", req.URL.EscapedPath())
|
t.Errorf("POST expected at: %q", req.URL.EscapedPath())
|
||||||
|
|
|
@ -28,7 +28,7 @@ func Actor(ctx *context.APIContext) {
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/ActivityPub"
|
// "$ref": "#/responses/ActivityPub"
|
||||||
|
|
||||||
link := user_model.APActorUserAPActorID()
|
link := user_model.APServerActorID()
|
||||||
actor := ap.ActorNew(ap.IRI(link), ap.ApplicationType)
|
actor := ap.ActorNew(ap.IRI(link), ap.ApplicationType)
|
||||||
|
|
||||||
actor.PreferredUsername = ap.NaturalLanguageValuesNew()
|
actor.PreferredUsername = ap.NaturalLanguageValuesNew()
|
||||||
|
@ -46,7 +46,7 @@ func Actor(ctx *context.APIContext) {
|
||||||
actor.PublicKey.ID = ap.IRI(link + "#main-key")
|
actor.PublicKey.ID = ap.IRI(link + "#main-key")
|
||||||
actor.PublicKey.Owner = ap.IRI(link)
|
actor.PublicKey.Owner = ap.IRI(link)
|
||||||
|
|
||||||
publicKeyPem, err := activitypub.GetPublicKey(ctx, user_model.NewAPActorUser())
|
publicKeyPem, err := activitypub.GetPublicKey(ctx, user_model.NewAPServerActor())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetPublicKey", err)
|
ctx.ServerError("GetPublicKey", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -6,59 +6,141 @@ package activitypub
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"database/sql"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"forgejo.org/models/db"
|
||||||
|
"forgejo.org/models/forgefed"
|
||||||
|
"forgejo.org/models/user"
|
||||||
"forgejo.org/modules/activitypub"
|
"forgejo.org/modules/activitypub"
|
||||||
"forgejo.org/modules/httplib"
|
fm "forgejo.org/modules/forgefed"
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
gitea_context "forgejo.org/services/context"
|
gitea_context "forgejo.org/services/context"
|
||||||
|
"forgejo.org/services/federation"
|
||||||
|
|
||||||
"github.com/42wim/httpsig"
|
"github.com/42wim/httpsig"
|
||||||
ap "github.com/go-ap/activitypub"
|
ap "github.com/go-ap/activitypub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {
|
func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
|
||||||
person := ap.PersonNew(ap.IRI(keyID.String()))
|
|
||||||
err = person.UnmarshalJSON(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
|
|
||||||
}
|
|
||||||
pubKey := person.PublicKey
|
|
||||||
if pubKey.ID.String() != keyID.String() {
|
|
||||||
return nil, fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b))
|
|
||||||
}
|
|
||||||
pubKeyPem := pubKey.PublicKeyPem
|
|
||||||
block, _ := pem.Decode([]byte(pubKeyPem))
|
block, _ := pem.Decode([]byte(pubKeyPem))
|
||||||
if block == nil || block.Type != "PUBLIC KEY" {
|
if block == nil || block.Type != "PUBLIC KEY" {
|
||||||
return nil, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
return nil, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||||
}
|
}
|
||||||
p, err = x509.ParsePKIXPublicKey(block.Bytes)
|
|
||||||
return p, err
|
return block.Bytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetch(iri *url.URL) (b []byte, err error) {
|
func getFederatedUser(ctx *gitea_context.APIContext, person *ap.Person, federationHost *forgefed.FederationHost) (*user.FederatedUser, error) {
|
||||||
req := httplib.NewRequest(iri.String(), http.MethodGet)
|
dbUser, err := user.GetUserByFederatedURI(ctx, person.ID.String())
|
||||||
req.Header("Accept", activitypub.ActivityStreamsContentType)
|
|
||||||
req.Header("User-Agent", "Gitea/"+setting.AppVer)
|
|
||||||
resp, err := req.Response()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if dbUser != nil {
|
||||||
return nil, fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status)
|
federatedUser, err := user.GetFederatedUserByUserID(ctx, dbUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if federatedUser != nil {
|
||||||
|
return federatedUser, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b, err = io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize))
|
|
||||||
return b, err
|
personID, err := fm.NewPersonID(person.ID.String(), string(federationHost.NodeInfo.SoftwareName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, federatedUser, err := federation.CreateUserFromAP(ctx, personID, federationHost.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return federatedUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func storePublicKey(ctx *gitea_context.APIContext, person *ap.Person, pubKeyBytes []byte) error {
|
||||||
|
federationHost, err := federation.GetFederationHostForURI(ctx, person.ID.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if person.Type == ap.ActivityVocabularyType("Application") {
|
||||||
|
federationHost.KeyID = sql.NullString{
|
||||||
|
String: person.PublicKey.ID.String(),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
federationHost.PublicKey = sql.Null[sql.RawBytes]{
|
||||||
|
V: pubKeyBytes,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).ID(federationHost.ID).Update(federationHost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if person.Type == ap.ActivityVocabularyType("Person") {
|
||||||
|
federatedUser, err := getFederatedUser(ctx, person, federationHost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
federatedUser.KeyID = sql.NullString{
|
||||||
|
String: person.PublicKey.ID.String(),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
federatedUser.PublicKey = sql.Null[sql.RawBytes]{
|
||||||
|
V: pubKeyBytes,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).ID(federatedUser.ID).Update(federatedUser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (person *ap.Person, pubKeyBytes []byte, p crypto.PublicKey, err error) {
|
||||||
|
person = ap.PersonNew(ap.IRI(keyID.String()))
|
||||||
|
err = person.UnmarshalJSON(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey := person.PublicKey
|
||||||
|
if pubKey.ID.String() != keyID.String() {
|
||||||
|
return nil, nil, nil, fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKeyBytes, err = decodePublicKeyPem(pubKey.PublicKeyPem)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err = x509.ParsePKIXPublicKey(pubKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return person, pubKeyBytes, p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) {
|
func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) {
|
||||||
|
if !setting.Federation.SignatureEnforced {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
r := ctx.Req
|
r := ctx.Req
|
||||||
|
|
||||||
// 1. Figure out what key we need to verify
|
// 1. Figure out what key we need to verify
|
||||||
|
@ -66,23 +148,78 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ID := v.KeyId()
|
ID := v.KeyId()
|
||||||
idIRI, err := url.Parse(ID)
|
idIRI, err := url.Parse(ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signatureAlgorithm := httpsig.Algorithm(setting.Federation.SignatureAlgorithms[0])
|
||||||
|
|
||||||
// 2. Fetch the public key of the other actor
|
// 2. Fetch the public key of the other actor
|
||||||
b, err := fetch(idIRI)
|
// Try if the signing actor is an already known federated user
|
||||||
|
federationUser, err := user.GetFederatedUserByKeyID(ctx, idIRI.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
pubKey, err := getPublicKeyFromResponse(b, idIRI)
|
|
||||||
|
if federationUser != nil && federationUser.PublicKey.Valid {
|
||||||
|
pubKey, err := x509.ParsePKIXPublicKey(federationUser.PublicKey.V)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||||
|
return authenticated, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try if the signing actor is an already known federation host
|
||||||
|
federationHost, err := forgefed.FindFederationHostByKeyID(ctx, idIRI.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
// 3. Verify the other actor's key
|
|
||||||
algo := httpsig.Algorithm(setting.Federation.Algorithms[0])
|
if federationHost != nil && federationHost.PublicKey.Valid {
|
||||||
authenticated = v.Verify(pubKey, algo) == nil
|
pubKey, err := x509.ParsePKIXPublicKey(federationHost.PublicKey.V)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||||
|
return authenticated, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch missing public key
|
||||||
|
actionsUser := user.NewAPServerActor()
|
||||||
|
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.APActorKeyID())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := apClient.GetBody(idIRI.String())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
person, pubKeyBytes, pubKey, err := getPublicKeyFromResponse(b, idIRI)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticated = v.Verify(pubKey, signatureAlgorithm) == nil
|
||||||
|
if authenticated {
|
||||||
|
err = storePublicKey(ctx, person, pubKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return authenticated, err
|
return authenticated, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -840,22 +840,22 @@ func Routes() *web.Route {
|
||||||
m.Group("/activitypub", func() {
|
m.Group("/activitypub", func() {
|
||||||
// deprecated, remove in 1.20, use /user-id/{user-id} instead
|
// deprecated, remove in 1.20, use /user-id/{user-id} instead
|
||||||
m.Group("/user/{username}", func() {
|
m.Group("/user/{username}", func() {
|
||||||
m.Get("", activitypub.Person)
|
m.Get("", activitypub.ReqHTTPSignature(), activitypub.Person)
|
||||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||||
m.Group("/user-id/{user-id}", func() {
|
m.Group("/user-id/{user-id}", func() {
|
||||||
m.Get("", activitypub.Person)
|
m.Get("", activitypub.ReqHTTPSignature(), activitypub.Person)
|
||||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||||
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
|
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
|
||||||
m.Group("/actor", func() {
|
m.Group("/actor", func() {
|
||||||
m.Get("", activitypub.Actor)
|
m.Get("", activitypub.Actor)
|
||||||
m.Post("/inbox", activitypub.ActorInbox)
|
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.ActorInbox)
|
||||||
})
|
})
|
||||||
m.Group("/repository-id/{repository-id}", func() {
|
m.Group("/repository-id/{repository-id}", func() {
|
||||||
m.Get("", activitypub.Repository)
|
m.Get("", activitypub.ReqHTTPSignature(), activitypub.Repository)
|
||||||
m.Post("/inbox",
|
m.Post("/inbox",
|
||||||
bind(forgefed.ForgeLike{}),
|
bind(forgefed.ForgeLike{}),
|
||||||
// TODO: activitypub.ReqHTTPSignature(),
|
activitypub.ReqHTTPSignature(),
|
||||||
activitypub.RepositoryInbox)
|
activitypub.RepositoryInbox)
|
||||||
}, context.RepositoryIDAssignmentAPI())
|
}, context.RepositoryIDAssignmentAPI())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
|
||||||
|
|
|
@ -1313,7 +1313,7 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special user that can't have associated contributions and permissions in the repo.
|
// Special user that can't have associated contributions and permissions in the repo.
|
||||||
if poster.IsGhost() || poster.IsActions() || poster.IsAPActor() {
|
if poster.IsGhost() || poster.IsActions() || poster.IsAPServerActor() {
|
||||||
return roleDescriptor, nil
|
return roleDescriptor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,39 +98,47 @@ func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forgefed.FederationHost, error) {
|
func CreateFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forgefed.FederationHost, error) {
|
||||||
actionsUser := user.NewActionsUser()
|
actionsUser := user.NewAPServerActor()
|
||||||
clientFactory, err := activitypub.GetClientFactory(ctx)
|
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
client, err := clientFactory.WithKeys(ctx, actionsUser, "no idea where to get key material.")
|
|
||||||
|
client, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.APActorKeyID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI())
|
body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeInfoWellKnown, err := forgefed.NewNodeInfoWellKnown(body)
|
nodeInfoWellKnown, err := forgefed.NewNodeInfoWellKnown(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err = client.GetBody(nodeInfoWellKnown.Href)
|
body, err = client.GetBody(nodeInfoWellKnown.Href)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeInfo, err := forgefed.NewNodeInfo(body)
|
nodeInfo, err := forgefed.NewNodeInfo(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := forgefed.NewFederationHost(nodeInfo, actorID.Host)
|
result, err := forgefed.NewFederationHost(nodeInfo, actorID.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = forgefed.CreateFederationHost(ctx, &result)
|
err = forgefed.CreateFederationHost(ctx, &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,18 +163,18 @@ func GetFederationHostForURI(ctx context.Context, actorURI string) (*forgefed.Fe
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
|
func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
|
||||||
// ToDo: Do we get a publicKeyId from server, repo or owner or repo?
|
actionsUser := user.NewAPServerActor()
|
||||||
actionsUser := user.NewActionsUser()
|
|
||||||
clientFactory, err := activitypub.GetClientFactory(ctx)
|
clientFactory, err := activitypub.GetClientFactory(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
client, err := clientFactory.WithKeys(ctx, actionsUser, "no idea where to get key material.")
|
|
||||||
|
apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.APActorKeyID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := client.GetBody(personID.AsURI())
|
body, err := apClient.GetBody(personID.AsURI())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -176,26 +184,32 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if res, err := validation.IsValid(person); !res {
|
if res, err := validation.IsValid(person); !res {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Fetched valid person:%q", person)
|
log.Info("Fetched valid person:%q", person)
|
||||||
|
|
||||||
localFqdn, err := url.ParseRequestURI(setting.AppURL)
|
localFqdn, err := url.ParseRequestURI(setting.AppURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname())
|
email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname())
|
||||||
loginName := personID.AsLoginName()
|
loginName := personID.AsLoginName()
|
||||||
name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix())
|
name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix())
|
||||||
fullName := person.Name.String()
|
fullName := person.Name.String()
|
||||||
|
|
||||||
if len(person.Name) == 0 {
|
if len(person.Name) == 0 {
|
||||||
fullName = name
|
fullName = name
|
||||||
}
|
}
|
||||||
|
|
||||||
password, err := password.Generate(32)
|
password, err := password.Generate(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
newUser := user.User{
|
newUser := user.User{
|
||||||
LowerName: strings.ToLower(name),
|
LowerName: strings.ToLower(name),
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -209,16 +223,18 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
|
||||||
IsAdmin: false,
|
IsAdmin: false,
|
||||||
NormalizedFederatedURI: personID.AsURI(),
|
NormalizedFederatedURI: personID.AsURI(),
|
||||||
}
|
}
|
||||||
|
|
||||||
federatedUser := user.FederatedUser{
|
federatedUser := user.FederatedUser{
|
||||||
ExternalID: personID.ID,
|
ExternalID: personID.ID,
|
||||||
FederationHostID: federationHostID,
|
FederationHostID: federationHostID,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
|
err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
log.Info("Created federatedUser:%q", federatedUser)
|
|
||||||
|
|
||||||
|
log.Info("Created federatedUser:%q", federatedUser)
|
||||||
return &newUser, &federatedUser, nil
|
return &newUser, &federatedUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,7 +290,8 @@ func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorID())
|
|
||||||
|
apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorKeyID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -285,7 +302,7 @@ func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = apclient.Post(json, fmt.Sprintf("%v/inbox/", activity.Object))
|
_, err = apclient.Post(json, fmt.Sprintf("%s/inbox", activity.Object))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("error %v while sending activity: %q", err, activity)
|
log.Error("error %v while sending activity: %q", err, activity)
|
||||||
}
|
}
|
||||||
|
|
54
tests/integration/activitypub_client_test.go
Normal file
54
tests/integration/activitypub_client_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.org/models/db"
|
||||||
|
"forgejo.org/models/unittest"
|
||||||
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/activitypub"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
|
"forgejo.org/routers"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActivityPubClientBodySize(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
|
|
||||||
|
clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.APActorKeyID())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
url := u.JoinPath("/api/v1/nodeinfo").String()
|
||||||
|
|
||||||
|
// Request with normal MaxSize
|
||||||
|
t.Run("NormalMaxSize", func(t *testing.T) {
|
||||||
|
resp, err := apClient.GetBody(url)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(resp), "forgejo")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set MaxSize to something very low to always fail
|
||||||
|
// Request with low MaxSize
|
||||||
|
t.Run("LowMaxSize", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Federation.MaxSize, 100)()
|
||||||
|
|
||||||
|
_, err = apClient.GetBody(url)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorContains(t, err, "Request returned")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -26,33 +26,47 @@ import (
|
||||||
func TestActivityPubPerson(t *testing.T) {
|
func TestActivityPubPerson(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
defer tests.PrepareTestEnv(t)()
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
userID := 2
|
||||||
|
username := "user2"
|
||||||
|
userURL := fmt.Sprintf("%sapi/v1/activitypub/user-id/%d", u, userID)
|
||||||
|
|
||||||
userID := 2
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
username := "user2"
|
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/user-id/%v", userID))
|
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
|
||||||
assert.Contains(t, resp.Body.String(), "@context")
|
|
||||||
|
|
||||||
var person ap.Person
|
clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||||
err := person.UnmarshalJSON(resp.Body.Bytes())
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, ap.PersonType, person.Type)
|
apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.APActorKeyID())
|
||||||
assert.Equal(t, username, person.PreferredUsername.String())
|
require.NoError(t, err)
|
||||||
keyID := person.GetID().String()
|
|
||||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v$", userID), keyID)
|
|
||||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v/outbox$", userID), person.Outbox.GetID().String())
|
|
||||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v/inbox$", userID), person.Inbox.GetID().String())
|
|
||||||
|
|
||||||
pubKey := person.PublicKey
|
// Unsigned request
|
||||||
assert.NotNil(t, pubKey)
|
t.Run("UnsignedRequest", func(t *testing.T) {
|
||||||
publicKeyID := keyID + "#main-key"
|
req := NewRequest(t, "GET", userURL)
|
||||||
assert.Equal(t, pubKey.ID.String(), publicKeyID)
|
MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
pubKeyPem := pubKey.PublicKeyPem
|
t.Run("SignedRequestValidation", func(t *testing.T) {
|
||||||
assert.NotNil(t, pubKeyPem)
|
// Signed requset
|
||||||
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
|
resp, err := apClient.GetBody(userURL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var person ap.Person
|
||||||
|
err = person.UnmarshalJSON(resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, ap.PersonType, person.Type)
|
||||||
|
assert.Equal(t, username, person.PreferredUsername.String())
|
||||||
|
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d$", userID), person.GetID())
|
||||||
|
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d/outbox$", userID), person.Outbox.GetID().String())
|
||||||
|
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d/inbox$", userID), person.Inbox.GetID().String())
|
||||||
|
|
||||||
|
assert.NotNil(t, person.PublicKey)
|
||||||
|
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%d#main-key$", userID), person.PublicKey.ID)
|
||||||
|
|
||||||
|
assert.NotNil(t, person.PublicKey.PublicKeyPem)
|
||||||
|
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", person.PublicKey.PublicKeyPem)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActivityPubMissingPerson(t *testing.T) {
|
func TestActivityPubMissingPerson(t *testing.T) {
|
||||||
|
|
|
@ -28,18 +28,28 @@ import (
|
||||||
func TestActivityPubRepository(t *testing.T) {
|
func TestActivityPubRepository(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
repositoryID := 2
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%v", repositoryID))
|
repositoryID := 2
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
|
||||||
assert.Contains(t, resp.Body.String(), "@context")
|
|
||||||
|
|
||||||
var repository forgefed_modules.Repository
|
apServerActor := user.NewAPServerActor()
|
||||||
err := repository.UnmarshalJSON(resp.Body.Bytes())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Regexp(t, fmt.Sprintf("activitypub/repository-id/%v$", repositoryID), repository.GetID().String())
|
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c, err := cf.WithKeys(db.DefaultContext, apServerActor, apServerActor.APActorKeyID())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := c.GetBody(fmt.Sprintf("%sapi/v1/activitypub/repository-id/%d", u, repositoryID))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(resp), "@context")
|
||||||
|
|
||||||
|
var repository forgefed_modules.Repository
|
||||||
|
err = repository.UnmarshalJSON(resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Regexp(t, fmt.Sprintf("activitypub/repository-id/%d$", repositoryID), repository.GetID().String())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActivityPubMissingRepository(t *testing.T) {
|
func TestActivityPubMissingRepository(t *testing.T) {
|
||||||
|
@ -48,7 +58,7 @@ func TestActivityPubMissingRepository(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
repositoryID := 9999999
|
repositoryID := 9999999
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%v", repositoryID))
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%d", repositoryID))
|
||||||
resp := MakeRequest(t, req, http.StatusNotFound)
|
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||||
assert.Contains(t, resp.Body.String(), "repository does not exist")
|
assert.Contains(t, resp.Body.String(), "repository does not exist")
|
||||||
}
|
}
|
||||||
|
@ -62,14 +72,16 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
|
||||||
defer federatedSrv.Close()
|
defer federatedSrv.Close()
|
||||||
|
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
actionsUser := user.NewActionsUser()
|
apServerActor := user.NewAPServerActor()
|
||||||
repositoryID := 2
|
repositoryID := 2
|
||||||
timeNow := time.Now().UTC()
|
timeNow := time.Now().UTC()
|
||||||
|
|
||||||
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
c, err := cf.WithKeys(db.DefaultContext, actionsUser, "not used")
|
|
||||||
|
c, err := cf.WithKeys(db.DefaultContext, apServerActor, apServerActor.APActorKeyID())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
repoInboxURL := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%d/inbox", repositoryID)).String()
|
repoInboxURL := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%d/inbox", repositoryID)).String()
|
||||||
|
|
||||||
activity1 := []byte(fmt.Sprintf(
|
activity1 := []byte(fmt.Sprintf(
|
||||||
|
@ -139,14 +151,16 @@ func TestActivityPubRepositoryInboxInvalid(t *testing.T) {
|
||||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
actionsUser := user.NewActionsUser()
|
apServerActor := user.NewAPServerActor()
|
||||||
repositoryID := 2
|
repositoryID := 2
|
||||||
|
|
||||||
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
cf, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
c, err := cf.WithKeys(db.DefaultContext, actionsUser, "not used")
|
|
||||||
|
c, err := cf.WithKeys(db.DefaultContext, apServerActor, apServerActor.APActorKeyID())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
repoInboxURL := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%v/inbox", repositoryID)).String()
|
repoInboxURL := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%d/inbox", repositoryID)).String()
|
||||||
activity := []byte(`{"type":"Wrong"}`)
|
activity := []byte(`{"type":"Wrong"}`)
|
||||||
resp, err := c.Post(activity, repoInboxURL)
|
resp, err := c.Post(activity, repoInboxURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
82
tests/integration/api_federation_httpsig_test.go
Normal file
82
tests/integration/api_federation_httpsig_test.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.org/models/db"
|
||||||
|
"forgejo.org/models/forgefed"
|
||||||
|
"forgejo.org/models/unittest"
|
||||||
|
"forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/activitypub"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/modules/test"
|
||||||
|
"forgejo.org/routers"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFederationHttpSigValidation(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
userID := 2
|
||||||
|
userURL := fmt.Sprintf("%sapi/v1/activitypub/user-id/%d", u, userID)
|
||||||
|
|
||||||
|
user1 := unittest.AssertExistsAndLoadBean(t, &user.User{ID: 1})
|
||||||
|
|
||||||
|
clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.APActorKeyID())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Unsigned request
|
||||||
|
t.Run("UnsignedRequest", func(t *testing.T) {
|
||||||
|
req := NewRequest(t, "GET", userURL)
|
||||||
|
MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Signed request
|
||||||
|
t.Run("SignedRequest", func(t *testing.T) {
|
||||||
|
resp, err := apClient.Get(userURL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
// HACK HACK HACK: the host part of the URL gets set to which IP forgejo is
|
||||||
|
// listening on, NOT localhost, which is the Domain given to forgejo which
|
||||||
|
// is then used for eg. the keyID all requests
|
||||||
|
applicationKeyID := fmt.Sprintf("%sapi/v1/activitypub/actor#main-key", setting.AppURL)
|
||||||
|
actorKeyID := fmt.Sprintf("%sapi/v1/activitypub/user-id/1#main-key", setting.AppURL)
|
||||||
|
|
||||||
|
// Check for cached public keys
|
||||||
|
t.Run("ValidateCaches", func(t *testing.T) {
|
||||||
|
host, err := forgefed.FindFederationHostByKeyID(db.DefaultContext, applicationKeyID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, host)
|
||||||
|
assert.True(t, host.PublicKey.Valid)
|
||||||
|
|
||||||
|
user, err := user.GetFederatedUserByKeyID(db.DefaultContext, actorKeyID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, user)
|
||||||
|
assert.True(t, user.PublicKey.Valid)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disable signature validation
|
||||||
|
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||||
|
|
||||||
|
// Unsigned request
|
||||||
|
t.Run("SignatureValidationDisabled", func(t *testing.T) {
|
||||||
|
req := NewRequest(t, "GET", userURL)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
109
tests/integration/user_federationhost_xorm_test.go
Normal file
109
tests/integration/user_federationhost_xorm_test.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.org/models/db"
|
||||||
|
"forgejo.org/models/forgefed"
|
||||||
|
"forgejo.org/models/user"
|
||||||
|
"forgejo.org/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStoreFederationHost(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
t.Run("ExplicitNull", func(t *testing.T) {
|
||||||
|
federationHost := forgefed.FederationHost{
|
||||||
|
HostFqdn: "ExplicitNull",
|
||||||
|
// Explicit null on KeyID and PublicKey
|
||||||
|
KeyID: sql.NullString{Valid: false},
|
||||||
|
PublicKey: sql.Null[sql.RawBytes]{Valid: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.GetEngine(db.DefaultContext).Insert(&federationHost)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dbFederationHost := new(forgefed.FederationHost)
|
||||||
|
has, err := db.GetEngine(db.DefaultContext).Where("host_fqdn=?", "ExplicitNull").Get(dbFederationHost)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, has)
|
||||||
|
|
||||||
|
assert.False(t, dbFederationHost.KeyID.Valid)
|
||||||
|
assert.False(t, dbFederationHost.PublicKey.Valid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NotNull", func(t *testing.T) {
|
||||||
|
federationHost := forgefed.FederationHost{
|
||||||
|
HostFqdn: "ImplicitNull",
|
||||||
|
KeyID: sql.NullString{Valid: true, String: "meow"},
|
||||||
|
PublicKey: sql.Null[sql.RawBytes]{Valid: true, V: sql.RawBytes{0x23, 0x42}},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.GetEngine(db.DefaultContext).Insert(&federationHost)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dbFederationHost := new(forgefed.FederationHost)
|
||||||
|
has, err := db.GetEngine(db.DefaultContext).Where("host_fqdn=?", "ImplicitNull").Get(dbFederationHost)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, has)
|
||||||
|
|
||||||
|
assert.True(t, dbFederationHost.KeyID.Valid)
|
||||||
|
assert.Equal(t, "meow", dbFederationHost.KeyID.String)
|
||||||
|
|
||||||
|
assert.True(t, dbFederationHost.PublicKey.Valid)
|
||||||
|
assert.Equal(t, sql.RawBytes{0x23, 0x42}, dbFederationHost.PublicKey.V)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStoreFederatedUser(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
t.Run("ExplicitNull", func(t *testing.T) {
|
||||||
|
federatedUser := user.FederatedUser{
|
||||||
|
UserID: 0,
|
||||||
|
ExternalID: "ExplicitNull",
|
||||||
|
FederationHostID: 0,
|
||||||
|
KeyID: sql.NullString{Valid: false},
|
||||||
|
PublicKey: sql.Null[sql.RawBytes]{Valid: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.GetEngine(db.DefaultContext).Insert(&federatedUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dbFederatedUser := new(user.FederatedUser)
|
||||||
|
has, err := db.GetEngine(db.DefaultContext).Where("user_id=?", 0).Get(dbFederatedUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, has)
|
||||||
|
|
||||||
|
assert.False(t, dbFederatedUser.KeyID.Valid)
|
||||||
|
assert.False(t, dbFederatedUser.PublicKey.Valid)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NotNull", func(t *testing.T) {
|
||||||
|
federatedUser := user.FederatedUser{
|
||||||
|
UserID: 1,
|
||||||
|
ExternalID: "ImplicitNull",
|
||||||
|
FederationHostID: 1,
|
||||||
|
KeyID: sql.NullString{Valid: true, String: "woem"},
|
||||||
|
PublicKey: sql.Null[sql.RawBytes]{Valid: true, V: sql.RawBytes{0x42, 0x23}},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.GetEngine(db.DefaultContext).Insert(&federatedUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dbFederatedUser := new(user.FederatedUser)
|
||||||
|
has, err := db.GetEngine(db.DefaultContext).Where("user_id=?", 1).Get(dbFederatedUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, has)
|
||||||
|
|
||||||
|
assert.True(t, dbFederatedUser.KeyID.Valid)
|
||||||
|
assert.Equal(t, "woem", dbFederatedUser.KeyID.String)
|
||||||
|
assert.True(t, dbFederatedUser.PublicKey.Valid)
|
||||||
|
assert.Equal(t, sql.RawBytes{0x42, 0x23}, dbFederatedUser.PublicKey.V)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue