From 91f7541addd573b3608cc36c63f03d994734bde7 Mon Sep 17 00:00:00 2001 From: Michael Jerger Date: Thu, 8 Feb 2024 13:31:27 +0100 Subject: [PATCH] refactor for semantic and cycle free deps --- models/user/user_repository.go | 33 +++- models/user/user_service.go | 86 ---------- modules/forgefed/federation_service.go | 202 +++++++++++++++++++++++ routers/api/v1/activitypub/repository.go | 165 +----------------- 4 files changed, 239 insertions(+), 247 deletions(-) delete mode 100644 models/user/user_service.go create mode 100644 modules/forgefed/federation_service.go diff --git a/models/user/user_repository.go b/models/user/user_repository.go index f4d34318fc..aead02970c 100644 --- a/models/user/user_repository.go +++ b/models/user/user_repository.go @@ -8,6 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" ) @@ -15,12 +16,38 @@ func init() { db.RegisterModel(new(FederatedUser)) } -func CreateFederationUser(ctx context.Context, user *FederatedUser) error { +func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error { if res, err := validation.IsValid(user); !res { + return fmt.Errorf("User is not valid: %v", err) + } + overwrite := CreateUserOverwriteOptions{ + IsActive: util.OptionalBoolFalse, + IsRestricted: util.OptionalBoolFalse, + } + + // 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 fmt.Errorf("FederatedUser is not valid: %v", err) } - _, err := db.GetEngine(ctx).Insert(user) - 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, diff --git a/models/user/user_service.go b/models/user/user_service.go deleted file mode 100644 index 44814ac2ff..0000000000 --- a/models/user/user_service.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2024 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package user - -import ( - "context" - "fmt" - "net/url" - "strings" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/forgefed" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" - "github.com/google/uuid" - pwd_gen "github.com/sethvargo/go-password/password" -) - -func CreateFederatedUserFromAP(ctx context.Context, person forgefed.ForgePerson, - personID forgefed.PersonID, federationHostID int64) (*User, *FederatedUser, error) { - - 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()) - log.Info("RepositoryInbox: person.Name: %v", person.Name) - fullName := person.Name.String() - if len(person.Name) == 0 { - fullName = name - } - - password, err := pwd_gen.Generate(32, 10, 10, false, true) - if err != nil { - return nil, nil, err - } - - user := User{ - LowerName: strings.ToLower(person.PreferredUsername.String()), - Name: name, - FullName: fullName, - Email: email, - EmailNotificationsPreference: "disabled", - Passwd: password, - MustChangePassword: false, - LoginName: loginName, - Type: UserTypeRemoteUser, - IsAdmin: false, - } - - overwrite := &CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolFalse, - IsRestricted: util.OptionalBoolFalse, - } - - // Begin transaction - ctx, committer, err := db.TxContext((ctx)) - if err != nil { - return nil, nil, err - } - defer committer.Close() - - if err := CreateUser(ctx, &user, overwrite); err != nil { - return nil, nil, err - } - - federatedUser, err := NewFederatedUser(user.ID, personID.ID, federationHostID) - if err != nil { - return nil, nil, err - } - - err = CreateFederationUser(ctx, &federatedUser) - if err != nil { - return nil, nil, err - } - - // Commit transaction - committer.Commit() - - return &user, &federatedUser, nil -} diff --git a/modules/forgefed/federation_service.go b/modules/forgefed/federation_service.go new file mode 100644 index 0000000000..27408550d9 --- /dev/null +++ b/modules/forgefed/federation_service.go @@ -0,0 +1,202 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + forgefed_model "code.gitea.io/gitea/models/forgefed" + "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "github.com/google/uuid" + + api "code.gitea.io/gitea/modules/activitypub" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/validation" + + pwd_gen "github.com/sethvargo/go-password/password" +) + +func LikeActivity(ctx *context.APIContext, form any, repositoryId int64) (error, int, string) { + activity := form.(*forgefed_model.ForgeLike) + if res, err := validation.IsValid(activity); !res { + return err, http.StatusNotAcceptable, "RepositoryInbox: Validate activity" + } + log.Info("RepositoryInbox: activity validated:%v", activity) + + // parse actorID (person) + actorURI := activity.Actor.GetID().String() + rawActorID, err := forgefed_model.NewActorID(actorURI) + if err != nil { + return err, http.StatusInternalServerError, "RepositoryInbox: Validating ActorID" + } + federationHost, err := forgefed_model.FindFederationHostByFqdn(ctx, rawActorID.Host) + if err != nil { + return err, http.StatusInternalServerError, "RepositoryInbox: Error while loading FederationInfo" + } + if federationHost == nil { + result, err := CreateFederationHostFromAP(ctx, rawActorID) + if err != nil { + return err, http.StatusNotAcceptable, "RepositoryInbox: Validate actorId" + } + federationHost = result + log.Info("RepositoryInbox: federationInfo validated: %v", federationHost) + } + if !activity.IsNewer(federationHost.LatestActivity) { + return fmt.Errorf("Activity already processed"), http.StatusNotAcceptable, "RepositoryInbox: Validate Activity" + } + + actorID, err := forgefed_model.NewPersonID(actorURI, string(federationHost.NodeInfo.Source)) + if err != nil { + return err, http.StatusNotAcceptable, "RepositoryInbox: Validate actorId" + } + log.Info("RepositoryInbox: actorId validated: %v", actorID) + // parse objectID (repository) + objectID, err := forgefed_model.NewRepositoryID(activity.Object.GetID().String(), string(forgefed_model.ForgejoSourceType)) + if err != nil { + return err, http.StatusNotAcceptable, "RepositoryInbox: Validate objectId" + } + if objectID.ID != fmt.Sprint(repositoryId) { + return err, http.StatusNotAcceptable, "RepositoryInbox: Validate objectId" + } + log.Info("RepositoryInbox: objectId validated: %v", objectID) + + actorAsLoginID := actorID.AsLoginName() // used as LoginName in newly created user + log.Info("RepositoryInbox: remoteStargazer: %v", actorAsLoginID) + + // Check if user already exists + user, _, err := user_model.FindFederatedUser(ctx, actorID.ID, federationHost.ID) + if err != nil { + return err, http.StatusInternalServerError, "RepositoryInbox: Searching for user failed" + } + if user != nil { + log.Info("RepositoryInbox: found user: %v", user) + } else { + user, _, err = CreateUserFromAP(ctx, actorID, federationHost.ID) + if err != nil { + return err, http.StatusInternalServerError, + "RepositoryInbox: Creating federated user failed" + } + log.Info("RepositoryInbox: created user from ap: %v", user) + } + + // execute the activity if the repo was not stared already + alreadyStared := repo.IsStaring(ctx, user.ID, repositoryId) + if !alreadyStared { + err = repo.StarRepo(ctx, user.ID, repositoryId, true) + if err != nil { + return err, http.StatusNotAcceptable, "RepositoryInbox: Star operation" + } + } + federationHost.LatestActivity = activity.StartTime + err = forgefed_model.UpdateFederationHost(ctx, *federationHost) + if err != nil { + return err, http.StatusNotAcceptable, "RepositoryInbox: error updateing federateionInfo" + } + + return nil, 0, "" +} + +func CreateFederationHostFromAP(ctx *context.APIContext, actorID forgefed_model.ActorID) (*forgefed_model.FederationHost, error) { + actionsUser := user_model.NewActionsUser() + client, err := api.NewClient(ctx, actionsUser, "no idea where to get key material.") + if err != nil { + return nil, err + } + body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI()) + if err != nil { + return nil, err + } + nodeInfoWellKnown, err := forgefed_model.NewNodeInfoWellKnown(body) + if err != nil { + return nil, err + } + body, err = client.GetBody(nodeInfoWellKnown.Href) + if err != nil { + return nil, err + } + nodeInfo, err := forgefed_model.NewNodeInfo(body) + if err != nil { + return nil, err + } + result, err := forgefed_model.NewFederationHost(nodeInfo, actorID.Host) + if err != nil { + return nil, err + } + err = forgefed_model.CreateFederationHost(ctx, result) + if err != nil { + return nil, err + } + return &result, nil +} + +func CreateUserFromAP(ctx *context.APIContext, personID forgefed_model.PersonID, federationHostID int64) (*user_model.User, *user_model.FederatedUser, error) { + // ToDo: Do we get a publicKeyId from server, repo or owner or repo? + actionsUser := user_model.NewActionsUser() + client, err := api.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 := forgefed_model.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("RepositoryInbox: validated 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()) + log.Info("RepositoryInbox: person.Name: %v", person.Name) + fullName := person.Name.String() + if len(person.Name) == 0 { + fullName = name + } + password, err := pwd_gen.Generate(32, 10, 10, false, true) + if err != nil { + return nil, nil, err + } + user := user_model.User{ + LowerName: strings.ToLower(person.PreferredUsername.String()), + Name: name, + FullName: fullName, + Email: email, + EmailNotificationsPreference: "disabled", + Passwd: password, + MustChangePassword: false, + LoginName: loginName, + Type: user_model.UserTypeRemoteUser, + IsAdmin: false, + } + + federatedUser := user_model.FederatedUser{ + ExternalID: personID.ID, + FederationHostID: federationHostID, + } + + err = user_model.CreateFederatedUser(ctx, &user, &federatedUser) + if err != nil { + return nil, nil, err + } + + return &user, &federatedUser, nil +} diff --git a/routers/api/v1/activitypub/repository.go b/routers/api/v1/activitypub/repository.go index ddfcab7151..36afe8e0ed 100644 --- a/routers/api/v1/activitypub/repository.go +++ b/routers/api/v1/activitypub/repository.go @@ -8,14 +8,12 @@ import ( "net/http" "strings" - "code.gitea.io/gitea/models/forgefed" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - api "code.gitea.io/gitea/modules/activitypub" + forgefed_model "code.gitea.io/gitea/models/forgefed" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/forgefed" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" ap "github.com/go-ap/activitypub" @@ -39,7 +37,7 @@ func Repository(ctx *context.APIContext) { // "$ref": "#/responses/ActivityPub" link := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.Repo.Repository.ID) - repo := forgefed.RepositoryNew(ap.IRI(link)) + repo := forgefed_model.RepositoryNew(ap.IRI(link)) repo.Name = ap.NaturalLanguageValuesNew() err := repo.Name.Set("en", ap.Content(ctx.Repo.Repository.Name)) @@ -75,160 +73,11 @@ func RepositoryInbox(ctx *context.APIContext) { repository := ctx.Repo.Repository log.Info("RepositoryInbox: repo: %v", repository) - activity := web.GetForm(ctx).(*forgefed.ForgeLike) - if res, err := validation.IsValid(activity); !res { - ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate activity", err) - return - } - log.Info("RepositoryInbox: activity validated:%v", activity) - - // parse actorID (person) - actorURI := activity.Actor.GetID().String() - rawActorID, err := forgefed.NewActorID(actorURI) + form := web.GetForm(ctx) + err, httpStatus, title := forgefed.LikeActivity(ctx, form, repository.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, - "RepositoryInbox: Validating ActorID", err) - return - } - federationHost, err := forgefed.FindFederationHostByFqdn(ctx, rawActorID.Host) - if err != nil { - ctx.Error(http.StatusInternalServerError, - "RepositoryInbox: Error while loading FederationInfo", err) - return - } - if federationHost == nil { - result, err := createFederationHost(ctx, rawActorID) - if err != nil { - ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate actorId", err) - return - } - federationHost = &result - log.Info("RepositoryInbox: federationInfo validated: %v", federationHost) - } - if !activity.IsNewer(federationHost.LatestActivity) { - ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate Activity", - fmt.Errorf("Activity already processed")) - return - } - - actorID, err := forgefed.NewPersonID(actorURI, string(federationHost.NodeInfo.Source)) - if err != nil { - ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate actorId", err) - return - } - log.Info("RepositoryInbox: actorId validated: %v", actorID) - // parse objectID (repository) - objectID, err := forgefed.NewRepositoryID(activity.Object.GetID().String(), string(forgefed.ForgejoSourceType)) - if err != nil { - ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate objectId", err) - return - } - if objectID.ID != fmt.Sprint(repository.ID) { - ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Validate objectId", err) - return - } - log.Info("RepositoryInbox: objectId validated: %v", objectID) - - actorAsLoginID := actorID.AsLoginName() // used as LoginName in newly created user - log.Info("RepositoryInbox: remoteStargazer: %v", actorAsLoginID) - - // Check if user already exists - user, _, err := user_model.FindFederatedUser(ctx, actorID.ID, federationHost.ID) - if err != nil { - ctx.Error(http.StatusInternalServerError, "RepositoryInbox: Searching for user failed", err) - return - } - if user != nil { - log.Info("RepositoryInbox: found user: %v", user) - } else { - user, err = createUserFromAP(ctx, actorID, federationHost.ID) - if err != nil { - ctx.Error(http.StatusInternalServerError, - "RepositoryInbox: Creating federated user failed", err) - return - } - log.Info("RepositoryInbox: created user from ap: %v", user) - } - - // execute the activity if the repo was not stared already - alreadyStared := repo_model.IsStaring(ctx, user.ID, repository.ID) - if !alreadyStared { - err = repo_model.StarRepo(ctx, user.ID, repository.ID, true) - if err != nil { - ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: Star operation", err) - return - } - } - federationHost.LatestActivity = activity.StartTime - err = forgefed.UpdateFederationHost(ctx, *federationHost) - if err != nil { - ctx.Error(http.StatusNotAcceptable, "RepositoryInbox: error updateing federateionInfo", err) - return + ctx.Error(httpStatus, title, err) } ctx.Status(http.StatusNoContent) } - -func createFederationHost(ctx *context.APIContext, actorID forgefed.ActorID) (forgefed.FederationHost, error) { - actionsUser := user_model.NewActionsUser() - client, err := api.NewClient(ctx, actionsUser, "no idea where to get key material.") - if err != nil { - return forgefed.FederationHost{}, err - } - body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI()) - if err != nil { - return forgefed.FederationHost{}, err - } - nodeInfoWellKnown, err := forgefed.NewNodeInfoWellKnown(body) - if err != nil { - return forgefed.FederationHost{}, err - } - body, err = client.GetBody(nodeInfoWellKnown.Href) - if err != nil { - return forgefed.FederationHost{}, err - } - nodeInfo, err := forgefed.NewNodeInfo(body) - if err != nil { - return forgefed.FederationHost{}, err - } - result, err := forgefed.NewFederationHost(nodeInfo, actorID.Host) - if err != nil { - return forgefed.FederationHost{}, err - } - err = forgefed.CreateFederationHost(ctx, result) - if err != nil { - return forgefed.FederationHost{}, err - } - return result, nil -} - -func createUserFromAP(ctx *context.APIContext, personID forgefed.PersonID, federationHostID int64) (*user_model.User, error) { - // ToDo: Do we get a publicKeyId from server, repo or owner or repo? - actionsUser := user_model.NewActionsUser() - client, err := api.NewClient(ctx, actionsUser, "no idea where to get key material.") - if err != nil { - return nil, err - } - - body, err := client.GetBody(personID.AsURI()) - if err != nil { - return nil, err - } - - person := forgefed.ForgePerson{} - err = person.UnmarshalJSON(body) - if err != nil { - return nil, err - } - if res, err := validation.IsValid(person); !res { - return nil, err - } - log.Info("RepositoryInbox: validated person: %q", person) - - user, _, err := user_model.CreateFederatedUserFromAP(ctx, person, personID, federationHostID) - if err != nil { - return nil, err - } - - return user, nil -}