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 2019 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
@ -131,6 +132,9 @@ type User struct {
|
|||
AvatarEmail string `xorm:"NOT NULL"`
|
||||
UseCustomAvatar bool
|
||||
|
||||
// For federation
|
||||
NormalizedFederatedURI string
|
||||
|
||||
// Counters
|
||||
NumFollowers int
|
||||
NumFollowing int `xorm:"NOT NULL DEFAULT 0"`
|
||||
|
@ -303,6 +307,11 @@ func (u *User) HTMLURL() string {
|
|||
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.
|
||||
func (u *User) OrganisationLink() string {
|
||||
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
||||
|
@ -834,6 +843,17 @@ func ValidateUser(u *User, cols ...string) error {
|
|||
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
|
||||
func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
|
||||
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 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user_test
|
||||
|
@ -107,6 +108,15 @@ func TestGetAllUsers(t *testing.T) {
|
|||
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) {
|
||||
defer tests.AddFixtures("models/user/fixtures/")()
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
|
|
@ -7,13 +7,19 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/forgefed"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
"code.gitea.io/gitea/modules/auth/password"
|
||||
fm "code.gitea.io/gitea/modules/forgefed"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
|
@ -96,3 +133,67 @@ func GetFederationHostForURI(ctx context.Context, actorURI string) (*forgefed.Fe
|
|||
}
|
||||
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