Merge pull request 'Creation of federated user' (#3792) from meissa/forgejo:forgejo-federated-pr3 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3792
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-05-22 11:11:42 +00:00
commit d8e21e673d
12 changed files with 386 additions and 24 deletions

View file

@ -131,11 +131,13 @@ package "code.gitea.io/gitea/models/user"
func (ErrUserInactive).Unwrap func (ErrUserInactive).Unwrap
func IsErrExternalLoginUserAlreadyExist func IsErrExternalLoginUserAlreadyExist
func IsErrExternalLoginUserNotExist func IsErrExternalLoginUserNotExist
func NewFederatedUser
func IsErrUserSettingIsNotExist func IsErrUserSettingIsNotExist
func GetUserAllSettings func GetUserAllSettings
func DeleteUserSetting func DeleteUserSetting
func GetUserEmailsByNames func GetUserEmailsByNames
func GetUserNamesByIDs func GetUserNamesByIDs
func DeleteFederatedUser
package "code.gitea.io/gitea/modules/activitypub" package "code.gitea.io/gitea/modules/activitypub"
func (*Client).Post func (*Client).Post
@ -169,16 +171,6 @@ package "code.gitea.io/gitea/modules/eventsource"
package "code.gitea.io/gitea/modules/forgefed" package "code.gitea.io/gitea/modules/forgefed"
func NewForgeLike func NewForgeLike
func NewPersonID
func (PersonID).AsWebfinger
func (PersonID).AsLoginName
func (PersonID).HostSuffix
func (PersonID).Validate
func NewRepositoryID
func (RepositoryID).Validate
func (ForgePerson).MarshalJSON
func (*ForgePerson).UnmarshalJSON
func (ForgePerson).Validate
func GetItemByType func GetItemByType
func JSONUnmarshalerFn func JSONUnmarshalerFn
func NotEmpty func NotEmpty

View file

@ -68,6 +68,10 @@ var migrations = []*Migration{
NewMigration("Remove Gitea-specific columns from the repository and badge tables", RemoveGiteaSpecificColumnsFromRepositoryAndBadge), NewMigration("Remove Gitea-specific columns from the repository and badge tables", RemoveGiteaSpecificColumnsFromRepositoryAndBadge),
// v15 -> v16 // v15 -> v16
NewMigration("Create the `federation_host` table", CreateFederationHostTable), NewMigration("Create the `federation_host` table", CreateFederationHostTable),
// v16 -> v17
NewMigration("Create the `federated_user` table", CreateFederatedUserTable),
// v17 -> v18
NewMigration("Add `normalized_federated_uri` column to `user` table", AddNormalizedFederatedURIToUser),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -0,0 +1,17 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgejo_migrations //nolint:revive
import "xorm.io/xorm"
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"`
}
func CreateFederatedUserTable(x *xorm.Engine) error {
return x.Sync(new(FederatedUser))
}

View file

@ -0,0 +1,14 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgejo_migrations //nolint:revive
import "xorm.io/xorm"
func AddNormalizedFederatedURIToUser(x *xorm.Engine) error {
type User struct {
ID int64 `xorm:"pk autoincr"`
NormalizedFederatedURI string
}
return x.Sync(&User{})
}

View file

@ -0,0 +1,35 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"code.gitea.io/gitea/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"`
}
func NewFederatedUser(userID int64, externalID string, federationHostID int64) (FederatedUser, error) {
result := FederatedUser{
UserID: userID,
ExternalID: externalID,
FederationHostID: federationHostID,
}
if valid, err := validation.IsValid(result); !valid {
return FederatedUser{}, err
}
return result, nil
}
func (user FederatedUser) Validate() []string {
var result []string
result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...)
result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...)
return result
}

View file

@ -0,0 +1,29 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"testing"
"code.gitea.io/gitea/modules/validation"
)
func Test_FederatedUserValidation(t *testing.T) {
sut := FederatedUser{
UserID: 12,
ExternalID: "12",
FederationHostID: 1,
}
if res, err := validation.IsValid(sut); !res {
t.Errorf("sut should be valid but was %q", err)
}
sut = FederatedUser{
ExternalID: "12",
FederationHostID: 1,
}
if res, _ := validation.IsValid(sut); res {
t.Errorf("sut should be invalid")
}
}

View file

@ -1,5 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved. // Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package user package user
@ -131,6 +132,9 @@ type User struct {
AvatarEmail string `xorm:"NOT NULL"` AvatarEmail string `xorm:"NOT NULL"`
UseCustomAvatar bool UseCustomAvatar bool
// For federation
NormalizedFederatedURI string
// Counters // Counters
NumFollowers int NumFollowers int
NumFollowing int `xorm:"NOT NULL DEFAULT 0"` NumFollowing int `xorm:"NOT NULL DEFAULT 0"`
@ -303,6 +307,11 @@ 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)
@ -834,6 +843,17 @@ func ValidateUser(u *User, cols ...string) error {
return nil return nil
} }
func (u User) Validate() []string {
var result []string
if err := ValidateUser(&u); err != nil {
result = append(result, err.Error())
}
if err := ValidateEmail(u.Email); err != nil {
result = append(result, err.Error())
}
return result
}
// UpdateUserCols update user according special columns // UpdateUserCols update user according special columns
func UpdateUserCols(ctx context.Context, u *User, cols ...string) error { func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
if err := ValidateUser(u, cols...); err != nil { if err := ValidateUser(u, cols...); err != nil {

View file

@ -0,0 +1,83 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/validation"
)
func init() {
db.RegisterModel(new(FederatedUser))
}
func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error {
if res, err := validation.IsValid(user); !res {
return err
}
overwrite := CreateUserOverwriteOptions{
IsActive: optional.Some(false),
IsRestricted: optional.Some(false),
}
// Begin transaction
ctx, committer, err := db.TxContext((ctx))
if err != nil {
return err
}
defer committer.Close()
if err := CreateUser(ctx, user, &overwrite); err != nil {
return err
}
federatedUser.UserID = user.ID
if res, err := validation.IsValid(federatedUser); !res {
return err
}
_, err = db.GetEngine(ctx).Insert(federatedUser)
if err != nil {
return err
}
// Commit transaction
return committer.Commit()
}
func FindFederatedUser(ctx context.Context, externalID string,
federationHostID int64,
) (*User, *FederatedUser, error) {
federatedUser := new(FederatedUser)
user := new(User)
has, err := db.GetEngine(ctx).Where("external_id=? and federation_host_id=?", externalID, federationHostID).Get(federatedUser)
if err != nil {
return nil, nil, err
} else if !has {
return nil, nil, nil
}
has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user)
if err != nil {
return nil, nil, err
} else if !has {
return nil, nil, fmt.Errorf("User %v for federated user is missing", federatedUser.UserID)
}
if res, err := validation.IsValid(*user); !res {
return nil, nil, err
}
if res, err := validation.IsValid(*federatedUser); !res {
return nil, nil, err
}
return user, federatedUser, nil
}
func DeleteFederatedUser(ctx context.Context, userID int64) error {
_, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID})
return err
}

View file

@ -1,4 +1,5 @@
// Copyright 2017 The Gitea Authors. All rights reserved. // Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package user_test package user_test
@ -107,6 +108,15 @@ func TestGetAllUsers(t *testing.T) {
assert.False(t, found[user_model.UserTypeOrganization], users) assert.False(t, found[user_model.UserTypeOrganization], users)
} }
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)
}
}
func TestSearchUsers(t *testing.T) { func TestSearchUsers(t *testing.T) {
defer tests.AddFixtures("models/user/fixtures/")() defer tests.AddFixtures("models/user/fixtures/")()
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View file

@ -74,9 +74,6 @@ func RepositoryInbox(ctx *context.APIContext) {
form := web.GetForm(ctx) form := web.GetForm(ctx)
httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID) httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID)
if err != nil { if err != nil {
log.Error("Status: %v", httpStatus)
log.Error("Title: %v", title)
log.Error("Error: %v", err)
ctx.Error(httpStatus, title, err) ctx.Error(httpStatus, title, err)
} }
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)

View file

@ -7,13 +7,19 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings"
"code.gitea.io/gitea/models/forgefed" "code.gitea.io/gitea/models/forgefed"
"code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/auth/password"
fm "code.gitea.io/gitea/modules/forgefed" fm "code.gitea.io/gitea/modules/forgefed"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/validation"
"github.com/google/uuid"
) )
// ProcessLikeActivity receives a ForgeLike activity and does the following: // ProcessLikeActivity receives a ForgeLike activity and does the following:
@ -40,6 +46,37 @@ func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int
if !activity.IsNewer(federationHost.LatestActivity) { if !activity.IsNewer(federationHost.LatestActivity) {
return http.StatusNotAcceptable, "Activity out of order.", fmt.Errorf("Activity already processed") return http.StatusNotAcceptable, "Activity out of order.", fmt.Errorf("Activity already processed")
} }
actorID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName))
if err != nil {
return http.StatusNotAcceptable, "Invalid PersonID", err
}
log.Info("Actor accepted:%v", actorID)
// parse objectID (repository)
objectID, err := fm.NewRepositoryID(activity.Object.GetID().String(), string(forgefed.ForgejoSourceType))
if err != nil {
return http.StatusNotAcceptable, "Invalid objectId", err
}
if objectID.ID != fmt.Sprint(repositoryID) {
return http.StatusNotAcceptable, "Invalid objectId", err
}
log.Info("Object accepted:%v", objectID)
// Check if user already exists
user, _, err := user.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
if err != nil {
return http.StatusInternalServerError, "Searching for user failed", err
}
if user != nil {
log.Info("Found local federatedUser: %v", user)
} else {
user, _, err = CreateUserFromAP(ctx, actorID, federationHost.ID)
if err != nil {
return http.StatusInternalServerError, "Error creating federatedUser", err
}
log.Info("Created federatedUser from ap: %v", user)
}
log.Info("Got user:%v", user.Name)
return 0, "", nil return 0, "", nil
} }
@ -96,3 +133,67 @@ func GetFederationHostForURI(ctx context.Context, actorURI string) (*forgefed.Fe
} }
return federationHost, nil return federationHost, nil
} }
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()
client, err := activitypub.NewClient(ctx, actionsUser, "no idea where to get key material.")
if err != nil {
return nil, nil, err
}
body, err := client.GetBody(personID.AsURI())
if err != nil {
return nil, nil, err
}
person := fm.ForgePerson{}
err = person.UnmarshalJSON(body)
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,
FullName: fullName,
Email: email,
EmailNotificationsPreference: "disabled",
Passwd: password,
MustChangePassword: false,
LoginName: loginName,
Type: user.UserTypeRemoteUser,
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)
return &newUser, &federatedUser, nil
}

View file

@ -91,15 +91,15 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
`"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`) `"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`)
fmt.Fprint(res, responseBody) fmt.Fprint(res, responseBody)
}) })
federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/2", federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/15",
func(res http.ResponseWriter, req *http.Request) { func(res http.ResponseWriter, req *http.Request) {
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2 // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2
responseBody := fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],` + responseBody := fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],` +
`"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2","type":"Person",` + `"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15","type":"Person",` +
`"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/1bb05d9a5f6675ed0272af9ea193063c"},` + `"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/1bb05d9a5f6675ed0272af9ea193063c"},` +
`"url":"https://federated-repo.prod.meissa.de/stargoose1","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2/inbox",` + `"url":"https://federated-repo.prod.meissa.de/stargoose1","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15/inbox",` +
`"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2/outbox","preferredUsername":"stargoose1",` + `"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15/outbox","preferredUsername":"stargoose1",` +
`"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2",` + `"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15",` +
`"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n` + `"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n` +
`CPiwhAqX3REvVq8tM79a2CEqZn9ka6vuXoDgBg/sBf/BUWqf7orkjUXwk/U0Egjf\nk5jcurF4vqf1u+rlAHH37dvSBaDjNj6Qnj4OP12bjfaY/yvs7+jue/eNXFHjzN4E\n` + `CPiwhAqX3REvVq8tM79a2CEqZn9ka6vuXoDgBg/sBf/BUWqf7orkjUXwk/U0Egjf\nk5jcurF4vqf1u+rlAHH37dvSBaDjNj6Qnj4OP12bjfaY/yvs7+jue/eNXFHjzN4E\n` +
`T2H4B/yeKTJ4UuAwTlLaNbZJul2baLlHelJPAsxiYaziVuV5P+IGWckY6RSerRaZ\nAkc4mmGGtjAyfN9aewe+lNVfwS7ElFx546PlLgdQgjmeSwLX8FWxbPE5A/PmaXCs\n` + `T2H4B/yeKTJ4UuAwTlLaNbZJul2baLlHelJPAsxiYaziVuV5P+IGWckY6RSerRaZ\nAkc4mmGGtjAyfN9aewe+lNVfwS7ElFx546PlLgdQgjmeSwLX8FWxbPE5A/PmaXCs\n` +
@ -107,6 +107,22 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
`LXX5AQ1xQNtlssnVoUBqBrvZsX2jUUKUocvZqMGuE4hfAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`) `LXX5AQ1xQNtlssnVoUBqBrvZsX2jUUKUocvZqMGuE4hfAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`)
fmt.Fprint(res, responseBody) fmt.Fprint(res, responseBody)
}) })
federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/30",
func(res http.ResponseWriter, req *http.Request) {
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/3
responseBody := fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],` +
`"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30","type":"Person",` +
`"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/9c03f03d1c1f13f21976a22489326fe1"},` +
`"url":"https://federated-repo.prod.meissa.de/stargoose2","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30/inbox",` +
`"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30/outbox","preferredUsername":"stargoose2",` +
`"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30",` +
`"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyv5NytsfqpWXSrwuk8a3\n0W1zE13QJioXb/e3opgN2CfKZkdm3hb+4+mGKoU/rCqegnL9/AO0Aw+R8fCHXx44\n` +
`iNkdVpdY8Dzq+tQ9IetPWbyVIBvSzGgvpqfS05JuVPsy8cBX9wByODjr5kq7k1/v\nY1G7E3uh0a/XJc+mZutwGC3gPgR93NSrqsvTPN4wdhCCu9uj02S8OBoKuSYaPkU+\n` +
`tZ4CEDpnclAOw/eNiH4x2irMvVtruEgtlTA5K2I4YJrmtGLidus47FCyc8/zEKUh\nAeiD8KWDvqsQgOhUwcQgRxAnYVCoMD9cnE+WFFRHTuQecNlmdNFs3Cr0yKcWjDde\n` +
`trvnehW7LfPveGb0tHRHPuVAJpncTOidUR5h/7pqMyvKHzuAHWomm9rEaGUxd/7a\nL1CFjAf39+QIEgu0Anj8mIc7CTiz+DQhDz+0jBOsQ0iDXc5GeBz7X9Xv4Jp966nq\n` +
`MUR0GQGXvfZQN9IqMO+WoUVy10Ddhns1EWGlA0x4fecnAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`)
fmt.Fprint(res, responseBody)
})
federatedRoutes.HandleFunc("/", federatedRoutes.HandleFunc("/",
func(res http.ResponseWriter, req *http.Request) { func(res http.ResponseWriter, req *http.Request) {
t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) t.Errorf("Unhandled request: %q", req.URL.EscapedPath())
@ -129,20 +145,64 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) {
"%s/api/v1/activitypub/repository-id/%v/inbox", "%s/api/v1/activitypub/repository-id/%v/inbox",
srv.URL, repositoryID) srv.URL, repositoryID)
activity := []byte(fmt.Sprintf( timeNow := time.Now().UTC()
activity1 := []byte(fmt.Sprintf(
`{"type":"Like",`+ `{"type":"Like",`+
`"startTime":"%s",`+ `"startTime":"%s",`+
`"actor":"%s/api/v1/activitypub/user-id/2",`+ `"actor":"%s/api/v1/activitypub/user-id/15",`+
`"object":"%s/api/v1/activitypub/repository-id/%v"}`, `"object":"%s/api/v1/activitypub/repository-id/%v"}`,
time.Now().UTC().Format(time.RFC3339), timeNow.Format(time.RFC3339),
federatedSrv.URL, srv.URL, repositoryID)) federatedSrv.URL, srv.URL, repositoryID))
t.Logf("activity: %s", activity) t.Logf("activity: %s", activity1)
resp, err := c.Post(activity, repoInboxURL) resp, err := c.Post(activity1, repoInboxURL)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode) assert.Equal(t, http.StatusNoContent, resp.StatusCode)
unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"}) federationHost := unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"})
federatedUser := unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "15", FederationHostID: federationHost.ID})
unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID})
// A like activity by a different user of the same federated host.
activity2 := []byte(fmt.Sprintf(
`{"type":"Like",`+
`"startTime":"%s",`+
`"actor":"%s/api/v1/activitypub/user-id/30",`+
`"object":"%s/api/v1/activitypub/repository-id/%v"}`,
// Make sure this activity happens later then the one before
timeNow.Add(time.Second).Format(time.RFC3339),
federatedSrv.URL, srv.URL, repositoryID))
t.Logf("activity: %s", activity2)
resp, err = c.Post(activity2, repoInboxURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
federatedUser = unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "30", FederationHostID: federationHost.ID})
unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID})
// The same user sends another like activity
otherRepositoryID := 3
otherRepoInboxURL := fmt.Sprintf(
"%s/api/v1/activitypub/repository-id/%v/inbox",
srv.URL, otherRepositoryID)
activity3 := []byte(fmt.Sprintf(
`{"type":"Like",`+
`"startTime":"%s",`+
`"actor":"%s/api/v1/activitypub/user-id/30",`+
`"object":"%s/api/v1/activitypub/repository-id/%v"}`,
// Make sure this activity happens later then the ones before
timeNow.Add(time.Second*2).Format(time.RFC3339),
federatedSrv.URL, srv.URL, otherRepositoryID))
t.Logf("activity: %s", activity3)
resp, err = c.Post(activity3, otherRepoInboxURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
federatedUser = unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "30", FederationHostID: federationHost.ID})
unittest.AssertExistsAndLoadBean(t, &user.User{ID: federatedUser.UserID})
}) })
} }