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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
| 
						 | 
				
			
			@ -15,12 +16,14 @@ import (
 | 
			
		|||
// FederationHost data type
 | 
			
		||||
// swagger:model
 | 
			
		||||
type FederationHost struct {
 | 
			
		||||
	ID             int64              `xorm:"pk autoincr"`
 | 
			
		||||
	HostFqdn       string             `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
 | 
			
		||||
	NodeInfo       NodeInfo           `xorm:"extends NOT NULL"`
 | 
			
		||||
	LatestActivity time.Time          `xorm:"NOT NULL"`
 | 
			
		||||
	Created        timeutil.TimeStamp `xorm:"created"`
 | 
			
		||||
	Updated        timeutil.TimeStamp `xorm:"updated"`
 | 
			
		||||
	ID             int64                  `xorm:"pk autoincr"`
 | 
			
		||||
	HostFqdn       string                 `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
 | 
			
		||||
	NodeInfo       NodeInfo               `xorm:"extends NOT NULL"`
 | 
			
		||||
	LatestActivity time.Time              `xorm:"NOT NULL"`
 | 
			
		||||
	Created        timeutil.TimeStamp     `xorm:"created"`
 | 
			
		||||
	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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,9 +30,9 @@ func GetFederationHost(ctx context.Context, ID int64) (*FederationHost, error) {
 | 
			
		|||
	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)
 | 
			
		||||
	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 {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else if !has {
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +44,14 @@ func FindFederationHostByFqdn(ctx context.Context, fqdn string) (*FederationHost
 | 
			
		|||
	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 {
 | 
			
		||||
	if res, err := validation.IsValid(host); !res {
 | 
			
		||||
		return err
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,6 +94,8 @@ var migrations = []*Migration{
 | 
			
		|||
	NewMigration("Add `created_unix` column to `user_redirect` table", AddCreatedUnixToRedirect),
 | 
			
		||||
	// v27 -> v28
 | 
			
		||||
	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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"database/sql"
 | 
			
		||||
 | 
			
		||||
	"forgejo.org/models/db"
 | 
			
		||||
	"forgejo.org/modules/validation"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type FederatedUser struct {
 | 
			
		||||
	ID               int64  `xorm:"pk autoincr"`
 | 
			
		||||
	UserID           int64  `xorm:"NOT NULL"`
 | 
			
		||||
	ExternalID       string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
 | 
			
		||||
	FederationHostID int64  `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
 | 
			
		||||
	ID               int64                  `xorm:"pk autoincr"`
 | 
			
		||||
	UserID           int64                  `xorm:"NOT NULL"`
 | 
			
		||||
	ExternalID       string                 `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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +32,30 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64) (
 | 
			
		|||
	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 {
 | 
			
		||||
	var result []string
 | 
			
		||||
	result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -311,11 +311,6 @@ func (u *User) HTMLURL() string {
 | 
			
		|||
	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.
 | 
			
		||||
func (u *User) OrganisationLink() string {
 | 
			
		||||
	return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,30 +73,30 @@ func (u *User) IsActions() bool {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	APActorUserID   = -3
 | 
			
		||||
	APActorUserName = "actor"
 | 
			
		||||
	APActorEmail    = "noreply@forgejo.org"
 | 
			
		||||
	APServerActorUserID   = -3
 | 
			
		||||
	APServerActorUserName = "actor"
 | 
			
		||||
	APServerActorEmail    = "noreply@forgejo.org"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewAPActorUser() *User {
 | 
			
		||||
func NewAPServerActor() *User {
 | 
			
		||||
	return &User{
 | 
			
		||||
		ID:               APActorUserID,
 | 
			
		||||
		Name:             APActorUserName,
 | 
			
		||||
		LowerName:        APActorUserName,
 | 
			
		||||
		ID:               APServerActorUserID,
 | 
			
		||||
		Name:             APServerActorUserName,
 | 
			
		||||
		LowerName:        APServerActorUserName,
 | 
			
		||||
		IsActive:         true,
 | 
			
		||||
		Email:            APActorEmail,
 | 
			
		||||
		Email:            APServerActorEmail,
 | 
			
		||||
		KeepEmailPrivate: true,
 | 
			
		||||
		LoginName:        APActorUserName,
 | 
			
		||||
		LoginName:        APServerActorUserName,
 | 
			
		||||
		Type:             UserTypeIndividual,
 | 
			
		||||
		Visibility:       structs.VisibleTypePublic,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func APActorUserAPActorID() string {
 | 
			
		||||
func APServerActorID() string {
 | 
			
		||||
	path, _ := url.JoinPath(setting.AppURL, "/api/v1/activitypub/actor")
 | 
			
		||||
	return path
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *User) IsAPActor() bool {
 | 
			
		||||
	return u != nil && u.ID == APActorUserID
 | 
			
		||||
func (u *User) IsAPServerActor() bool {
 | 
			
		||||
	return u != nil && u.ID == APServerActorUserID
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -139,9 +139,21 @@ func TestAPActorID(t *testing.T) {
 | 
			
		|||
	user := user_model.User{ID: 1}
 | 
			
		||||
	url := user.APActorID()
 | 
			
		||||
	expected := "https://try.gitea.io/api/v1/activitypub/user-id/1"
 | 
			
		||||
	if url != expected {
 | 
			
		||||
		t.Errorf("unexpected APActorID, expected: %q, actual: %q", expected, url)
 | 
			
		||||
	}
 | 
			
		||||
	assert.Equal(t, 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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -191,10 +191,17 @@ func (c *Client) GetBody(uri string) ([]byte, error) {
 | 
			
		|||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	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 {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug("Client: got body: %v", charLimiter(string(body), 120))
 | 
			
		||||
	return body, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,18 +15,20 @@ var (
 | 
			
		|||
		Enabled             bool
 | 
			
		||||
		ShareUserStatistics bool
 | 
			
		||||
		MaxSize             int64
 | 
			
		||||
		Algorithms          []string
 | 
			
		||||
		SignatureAlgorithms []string
 | 
			
		||||
		DigestAlgorithm     string
 | 
			
		||||
		GetHeaders          []string
 | 
			
		||||
		PostHeaders         []string
 | 
			
		||||
		SignatureEnforced   bool
 | 
			
		||||
	}{
 | 
			
		||||
		Enabled:             false,
 | 
			
		||||
		ShareUserStatistics: true,
 | 
			
		||||
		MaxSize:             4,
 | 
			
		||||
		Algorithms:          []string{"rsa-sha256", "rsa-sha512", "ed25519"},
 | 
			
		||||
		SignatureAlgorithms: []string{"rsa-sha256", "rsa-sha512", "ed25519"},
 | 
			
		||||
		DigestAlgorithm:     "SHA-256",
 | 
			
		||||
		GetHeaders:          []string{"(request-target)", "Date", "Host"},
 | 
			
		||||
		PostHeaders:         []string{"(request-target)", "Date", "Host", "Digest"},
 | 
			
		||||
		SignatureEnforced:   true,
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,8 +46,8 @@ func loadFederationFrom(rootCfg ConfigProvider) {
 | 
			
		|||
	// Get MaxSize in bytes instead of MiB
 | 
			
		||||
	Federation.MaxSize = 1 << 20 * Federation.MaxSize
 | 
			
		||||
 | 
			
		||||
	HttpsigAlgs = make([]httpsig.Algorithm, len(Federation.Algorithms))
 | 
			
		||||
	for i, alg := range Federation.Algorithms {
 | 
			
		||||
	HttpsigAlgs = make([]httpsig.Algorithm, len(Federation.SignatureAlgorithms))
 | 
			
		||||
	for i, alg := range Federation.SignatureAlgorithms {
 | 
			
		||||
		HttpsigAlgs[i] = httpsig.Algorithm(alg)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,7 +95,7 @@ func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server {
 | 
			
		|||
			})
 | 
			
		||||
	}
 | 
			
		||||
	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) {
 | 
			
		||||
				if req.Method != "POST" {
 | 
			
		||||
					t.Errorf("POST expected at: %q", req.URL.EscapedPath())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ func Actor(ctx *context.APIContext) {
 | 
			
		|||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/ActivityPub"
 | 
			
		||||
 | 
			
		||||
	link := user_model.APActorUserAPActorID()
 | 
			
		||||
	link := user_model.APServerActorID()
 | 
			
		||||
	actor := ap.ActorNew(ap.IRI(link), ap.ApplicationType)
 | 
			
		||||
 | 
			
		||||
	actor.PreferredUsername = ap.NaturalLanguageValuesNew()
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ func Actor(ctx *context.APIContext) {
 | 
			
		|||
	actor.PublicKey.ID = ap.IRI(link + "#main-key")
 | 
			
		||||
	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 {
 | 
			
		||||
		ctx.ServerError("GetPublicKey", err)
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,59 +6,141 @@ package activitypub
 | 
			
		|||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"forgejo.org/models/db"
 | 
			
		||||
	"forgejo.org/models/forgefed"
 | 
			
		||||
	"forgejo.org/models/user"
 | 
			
		||||
	"forgejo.org/modules/activitypub"
 | 
			
		||||
	"forgejo.org/modules/httplib"
 | 
			
		||||
	fm "forgejo.org/modules/forgefed"
 | 
			
		||||
	"forgejo.org/modules/log"
 | 
			
		||||
	"forgejo.org/modules/setting"
 | 
			
		||||
	gitea_context "forgejo.org/services/context"
 | 
			
		||||
	"forgejo.org/services/federation"
 | 
			
		||||
 | 
			
		||||
	"github.com/42wim/httpsig"
 | 
			
		||||
	ap "github.com/go-ap/activitypub"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err 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
 | 
			
		||||
func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
 | 
			
		||||
	block, _ := pem.Decode([]byte(pubKeyPem))
 | 
			
		||||
	if block == nil || block.Type != "PUBLIC KEY" {
 | 
			
		||||
		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) {
 | 
			
		||||
	req := httplib.NewRequest(iri.String(), http.MethodGet)
 | 
			
		||||
	req.Header("Accept", activitypub.ActivityStreamsContentType)
 | 
			
		||||
	req.Header("User-Agent", "Gitea/"+setting.AppVer)
 | 
			
		||||
	resp, err := req.Response()
 | 
			
		||||
func getFederatedUser(ctx *gitea_context.APIContext, person *ap.Person, federationHost *forgefed.FederationHost) (*user.FederatedUser, error) {
 | 
			
		||||
	dbUser, err := user.GetUserByFederatedURI(ctx, person.ID.String())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		return nil, fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status)
 | 
			
		||||
	if dbUser != nil {
 | 
			
		||||
		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) {
 | 
			
		||||
	if !setting.Federation.SignatureEnforced {
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r := ctx.Req
 | 
			
		||||
 | 
			
		||||
	// 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 {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ID := v.KeyId()
 | 
			
		||||
	idIRI, err := url.Parse(ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	signatureAlgorithm := httpsig.Algorithm(setting.Federation.SignatureAlgorithms[0])
 | 
			
		||||
 | 
			
		||||
	// 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 {
 | 
			
		||||
		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 {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	// 3. Verify the other actor's key
 | 
			
		||||
	algo := httpsig.Algorithm(setting.Federation.Algorithms[0])
 | 
			
		||||
	authenticated = v.Verify(pubKey, algo) == nil
 | 
			
		||||
 | 
			
		||||
	if federationHost != nil && federationHost.PublicKey.Valid {
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -840,22 +840,22 @@ func Routes() *web.Route {
 | 
			
		|||
			m.Group("/activitypub", func() {
 | 
			
		||||
				// deprecated, remove in 1.20, use /user-id/{user-id} instead
 | 
			
		||||
				m.Group("/user/{username}", func() {
 | 
			
		||||
					m.Get("", activitypub.Person)
 | 
			
		||||
					m.Get("", activitypub.ReqHTTPSignature(), activitypub.Person)
 | 
			
		||||
					m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
 | 
			
		||||
				}, context.UserAssignmentAPI(), checkTokenPublicOnly())
 | 
			
		||||
				m.Group("/user-id/{user-id}", func() {
 | 
			
		||||
					m.Get("", activitypub.Person)
 | 
			
		||||
					m.Get("", activitypub.ReqHTTPSignature(), activitypub.Person)
 | 
			
		||||
					m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
 | 
			
		||||
				}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
 | 
			
		||||
				m.Group("/actor", func() {
 | 
			
		||||
					m.Get("", activitypub.Actor)
 | 
			
		||||
					m.Post("/inbox", activitypub.ActorInbox)
 | 
			
		||||
					m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.ActorInbox)
 | 
			
		||||
				})
 | 
			
		||||
				m.Group("/repository-id/{repository-id}", func() {
 | 
			
		||||
					m.Get("", activitypub.Repository)
 | 
			
		||||
					m.Get("", activitypub.ReqHTTPSignature(), activitypub.Repository)
 | 
			
		||||
					m.Post("/inbox",
 | 
			
		||||
						bind(forgefed.ForgeLike{}),
 | 
			
		||||
						// TODO: activitypub.ReqHTTPSignature(),
 | 
			
		||||
						activitypub.ReqHTTPSignature(),
 | 
			
		||||
						activitypub.RepositoryInbox)
 | 
			
		||||
				}, context.RepositoryIDAssignmentAPI())
 | 
			
		||||
			}, 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.
 | 
			
		||||
	if poster.IsGhost() || poster.IsActions() || poster.IsAPActor() {
 | 
			
		||||
	if poster.IsGhost() || poster.IsActions() || poster.IsAPServerActor() {
 | 
			
		||||
		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) {
 | 
			
		||||
	actionsUser := user.NewActionsUser()
 | 
			
		||||
	actionsUser := user.NewAPServerActor()
 | 
			
		||||
	clientFactory, err := activitypub.GetClientFactory(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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 {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nodeInfoWellKnown, err := forgefed.NewNodeInfoWellKnown(body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, err = client.GetBody(nodeInfoWellKnown.Href)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nodeInfo, err := forgefed.NewNodeInfo(body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := forgefed.NewFederationHost(nodeInfo, actorID.Host)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = forgefed.CreateFederationHost(ctx, &result)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	// ToDo: Do we get a publicKeyId from server, repo or owner or repo?
 | 
			
		||||
	actionsUser := user.NewActionsUser()
 | 
			
		||||
	actionsUser := user.NewAPServerActor()
 | 
			
		||||
	clientFactory, err := activitypub.GetClientFactory(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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 {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, err := client.GetBody(personID.AsURI())
 | 
			
		||||
	body, err := apClient.GetBody(personID.AsURI())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -176,26 +184,32 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res, err := validation.IsValid(person); !res {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Fetched valid person:%q", person)
 | 
			
		||||
 | 
			
		||||
	localFqdn, err := url.ParseRequestURI(setting.AppURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname())
 | 
			
		||||
	loginName := personID.AsLoginName()
 | 
			
		||||
	name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix())
 | 
			
		||||
	fullName := person.Name.String()
 | 
			
		||||
 | 
			
		||||
	if len(person.Name) == 0 {
 | 
			
		||||
		fullName = name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	password, err := password.Generate(32)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newUser := user.User{
 | 
			
		||||
		LowerName:                    strings.ToLower(name),
 | 
			
		||||
		Name:                         name,
 | 
			
		||||
| 
						 | 
				
			
			@ -209,16 +223,18 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
 | 
			
		|||
		IsAdmin:                      false,
 | 
			
		||||
		NormalizedFederatedURI:       personID.AsURI(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	federatedUser := user.FederatedUser{
 | 
			
		||||
		ExternalID:       personID.ID,
 | 
			
		||||
		FederationHostID: federationHostID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	log.Info("Created federatedUser:%q", federatedUser)
 | 
			
		||||
 | 
			
		||||
	log.Info("Created federatedUser:%q", federatedUser)
 | 
			
		||||
	return &newUser, &federatedUser, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -274,7 +290,8 @@ func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorID())
 | 
			
		||||
 | 
			
		||||
	apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorKeyID())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -285,7 +302,7 @@ func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error
 | 
			
		|||
			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 {
 | 
			
		||||
			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) {
 | 
			
		||||
	defer test.MockVariableValue(&setting.Federation.Enabled, true)()
 | 
			
		||||
	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
 | 
			
		||||
	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")
 | 
			
		||||
		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 | 
			
		||||
 | 
			
		||||
	var person ap.Person
 | 
			
		||||
	err := person.UnmarshalJSON(resp.Body.Bytes())
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
		clientFactory, err := activitypub.GetClientFactory(db.DefaultContext)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, ap.PersonType, person.Type)
 | 
			
		||||
	assert.Equal(t, username, person.PreferredUsername.String())
 | 
			
		||||
	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())
 | 
			
		||||
		apClient, err := clientFactory.WithKeys(db.DefaultContext, user1, user1.APActorKeyID())
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	pubKey := person.PublicKey
 | 
			
		||||
	assert.NotNil(t, pubKey)
 | 
			
		||||
	publicKeyID := keyID + "#main-key"
 | 
			
		||||
	assert.Equal(t, pubKey.ID.String(), publicKeyID)
 | 
			
		||||
		// Unsigned request
 | 
			
		||||
		t.Run("UnsignedRequest", func(t *testing.T) {
 | 
			
		||||
			req := NewRequest(t, "GET", userURL)
 | 
			
		||||
			MakeRequest(t, req, http.StatusBadRequest)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
	pubKeyPem := pubKey.PublicKeyPem
 | 
			
		||||
	assert.NotNil(t, pubKeyPem)
 | 
			
		||||
	assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
 | 
			
		||||
		t.Run("SignedRequestValidation", func(t *testing.T) {
 | 
			
		||||
			// Signed requset
 | 
			
		||||
			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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,18 +28,28 @@ import (
 | 
			
		|||
func TestActivityPubRepository(t *testing.T) {
 | 
			
		||||
	defer test.MockVariableValue(&setting.Federation.Enabled, true)()
 | 
			
		||||
	defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	repositoryID := 2
 | 
			
		||||
	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/repository-id/%v", repositoryID))
 | 
			
		||||
	resp := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	assert.Contains(t, resp.Body.String(), "@context")
 | 
			
		||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
			
		||||
		repositoryID := 2
 | 
			
		||||
 | 
			
		||||
	var repository forgefed_modules.Repository
 | 
			
		||||
	err := repository.UnmarshalJSON(resp.Body.Bytes())
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
		apServerActor := user.NewAPServerActor()
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +58,7 @@ func TestActivityPubMissingRepository(t *testing.T) {
 | 
			
		|||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	assert.Contains(t, resp.Body.String(), "repository does not exist")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -62,14 +72,16 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
 | 
			
		|||
	defer federatedSrv.Close()
 | 
			
		||||
 | 
			
		||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
			
		||||
		actionsUser := user.NewActionsUser()
 | 
			
		||||
		apServerActor := user.NewAPServerActor()
 | 
			
		||||
		repositoryID := 2
 | 
			
		||||
		timeNow := time.Now().UTC()
 | 
			
		||||
 | 
			
		||||
		cf, err := activitypub.GetClientFactory(db.DefaultContext)
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
		repoInboxURL := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%d/inbox", repositoryID)).String()
 | 
			
		||||
 | 
			
		||||
		activity1 := []byte(fmt.Sprintf(
 | 
			
		||||
| 
						 | 
				
			
			@ -139,14 +151,16 @@ func TestActivityPubRepositoryInboxInvalid(t *testing.T) {
 | 
			
		|||
	defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
 | 
			
		||||
 | 
			
		||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
			
		||||
		actionsUser := user.NewActionsUser()
 | 
			
		||||
		apServerActor := user.NewAPServerActor()
 | 
			
		||||
		repositoryID := 2
 | 
			
		||||
 | 
			
		||||
		cf, err := activitypub.GetClientFactory(db.DefaultContext)
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
		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"}`)
 | 
			
		||||
		resp, err := c.Post(activity, repoInboxURL)
 | 
			
		||||
		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