mirror of
1
Fork 0
This commit is contained in:
Michael Jerger 2024-05-16 08:15:43 +02:00
parent fe3473fc8b
commit 1c7a9b00be
6 changed files with 278 additions and 0 deletions

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)
} }
// APAPIURL returns the IRI to the api endpoint of the user
func (u *User) APAPIURL() 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 TestAPAPIURL(t *testing.T) {
user := user_model.User{ID: 1}
url := user.APAPIURL()
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1"
if url != expected {
t.Errorf("unexpected APAPIURL, 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

@ -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(person.PreferredUsername.String()),
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
}