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:
commit
d8e21e673d
12 changed files with 386 additions and 24 deletions
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
17
models/forgejo_migrations/v16.go
Normal file
17
models/forgejo_migrations/v16.go
Normal 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))
|
||||||
|
}
|
14
models/forgejo_migrations/v17.go
Normal file
14
models/forgejo_migrations/v17.go
Normal 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{})
|
||||||
|
}
|
35
models/user/federated_user.go
Normal file
35
models/user/federated_user.go
Normal 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
|
||||||
|
}
|
29
models/user/federated_user_test.go
Normal file
29
models/user/federated_user_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
83
models/user/user_repository.go
Normal file
83
models/user/user_repository.go
Normal 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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue