initial
This commit is contained in:
parent
fe3473fc8b
commit
1c7a9b00be
6 changed files with 278 additions and 0 deletions
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
|
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 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())
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue