From 2f2c306a719f603840cdb125ec865d92b1898b8a Mon Sep 17 00:00:00 2001 From: koalajoe23 Date: Fri, 5 Sep 2025 19:45:46 +0200 Subject: [PATCH 1/7] check user and repo for redirects when using git via SSH transport --- cmd/serv.go | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/cmd/serv.go b/cmd/serv.go index 089d0e3bb7c60..8f7b1dbf41c99 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -20,6 +20,7 @@ import ( git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfstransfer" @@ -227,12 +228,49 @@ func runServ(ctx context.Context, c *cli.Command) error { } username := repoPathFields[0] + var uid int64 + + if err := initDB(ctx); err != nil { + return fail(ctx, "DB initialization error", "Error while initializing Gitea database") + } + + real_uid, err := user_model.LookupUserRedirect(ctx, username) + if err == nil { + uid = real_uid + } else { + user, err := user_model.GetUserByName(ctx, username) + if err == nil { + uid = user.ID + } else { + return fail(ctx, "Invalid username", "Could not find user or org: %s", username) + } + } + + //We need the uid for repo redirect lookup + real_user, err := user_model.GetUserByID(ctx, uid) + if err == nil { + username = real_user.Name + } else { + return fail(ctx, "User ID lookup failed", "Could not find user with ID: %d", uid) + } + reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki" + real_rid, err := repo.LookupRedirect(ctx, uid, reponame) + if err == nil { + real_repo, err := repo.GetRepositoryByID(ctx, real_rid) + if err == nil { + reponame = real_repo.Name + username = real_repo.OwnerName + } else { + return fail(ctx, "Repo ID lookup failed", "Could not find repo with ID: %d", real_rid) + } + } + // LowerCase and trim the repoPath as that's how they are stored. // This should be done after splitting the repoPath into username and reponame // so that username and reponame are not affected. - repoPath = strings.ToLower(strings.TrimSpace(repoPath)) + repoPath = strings.ToLower(username + "/" + reponame) if !repo.IsValidSSHAccessRepoName(reponame) { return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) From 1859577db6e4b7f487b8afd4ef612fb46e37ec90 Mon Sep 17 00:00:00 2001 From: koalajoe23 Date: Fri, 5 Sep 2025 20:06:17 +0200 Subject: [PATCH 2/7] fix linting error --- cmd/serv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/serv.go b/cmd/serv.go index 8f7b1dbf41c99..2a2515487b3d5 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -246,7 +246,7 @@ func runServ(ctx context.Context, c *cli.Command) error { } } - //We need the uid for repo redirect lookup + // We need the uid for repo redirect lookup real_user, err := user_model.GetUserByID(ctx, uid) if err == nil { username = real_user.Name From d56d255f344f3ed72d62b6a5b8cd643953879389 Mon Sep 17 00:00:00 2001 From: koalajoe Date: Fri, 5 Sep 2025 21:00:45 +0000 Subject: [PATCH 3/7] var naming to satisfy linter --- cmd/serv.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/serv.go b/cmd/serv.go index 2a2515487b3d5..d7b031595451d 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -228,42 +228,42 @@ func runServ(ctx context.Context, c *cli.Command) error { } username := repoPathFields[0] - var uid int64 + var userID int64 if err := initDB(ctx); err != nil { return fail(ctx, "DB initialization error", "Error while initializing Gitea database") } - real_uid, err := user_model.LookupUserRedirect(ctx, username) + redirectedUserID, err := user_model.LookupUserRedirect(ctx, username) if err == nil { - uid = real_uid + userID = redirectedUserID } else { user, err := user_model.GetUserByName(ctx, username) if err == nil { - uid = user.ID + userID = user.ID } else { return fail(ctx, "Invalid username", "Could not find user or org: %s", username) } } // We need the uid for repo redirect lookup - real_user, err := user_model.GetUserByID(ctx, uid) + real_user, err := user_model.GetUserByID(ctx, userID) if err == nil { username = real_user.Name } else { - return fail(ctx, "User ID lookup failed", "Could not find user with ID: %d", uid) + return fail(ctx, "User ID lookup failed", "Could not find user with ID: %d", userID) } reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki" - real_rid, err := repo.LookupRedirect(ctx, uid, reponame) + redirectedRepoID, err := repo.LookupRedirect(ctx, userID, reponame) if err == nil { - real_repo, err := repo.GetRepositoryByID(ctx, real_rid) + redirectedRepo, err := repo.GetRepositoryByID(ctx, redirectedRepoID) if err == nil { - reponame = real_repo.Name - username = real_repo.OwnerName + reponame = redirectedRepo.Name + username = redirectedRepo.OwnerName } else { - return fail(ctx, "Repo ID lookup failed", "Could not find repo with ID: %d", real_rid) + return fail(ctx, "Repo ID lookup failed", "Could not find repo with ID: %d", redirectedRepoID) } } From 72544d662151e8a048fb24d1ae73d00eb7c3e496 Mon Sep 17 00:00:00 2001 From: koalajoe Date: Fri, 5 Sep 2025 23:09:50 +0000 Subject: [PATCH 4/7] Move redirection handling to private serv router --- cmd/serv.go | 48 +++++------------------------------------ routers/private/serv.go | 26 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/cmd/serv.go b/cmd/serv.go index d7b031595451d..4c9aa04be9e79 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -20,7 +20,6 @@ import ( git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfstransfer" @@ -228,50 +227,8 @@ func runServ(ctx context.Context, c *cli.Command) error { } username := repoPathFields[0] - var userID int64 - - if err := initDB(ctx); err != nil { - return fail(ctx, "DB initialization error", "Error while initializing Gitea database") - } - - redirectedUserID, err := user_model.LookupUserRedirect(ctx, username) - if err == nil { - userID = redirectedUserID - } else { - user, err := user_model.GetUserByName(ctx, username) - if err == nil { - userID = user.ID - } else { - return fail(ctx, "Invalid username", "Could not find user or org: %s", username) - } - } - - // We need the uid for repo redirect lookup - real_user, err := user_model.GetUserByID(ctx, userID) - if err == nil { - username = real_user.Name - } else { - return fail(ctx, "User ID lookup failed", "Could not find user with ID: %d", userID) - } - reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki" - redirectedRepoID, err := repo.LookupRedirect(ctx, userID, reponame) - if err == nil { - redirectedRepo, err := repo.GetRepositoryByID(ctx, redirectedRepoID) - if err == nil { - reponame = redirectedRepo.Name - username = redirectedRepo.OwnerName - } else { - return fail(ctx, "Repo ID lookup failed", "Could not find repo with ID: %d", redirectedRepoID) - } - } - - // LowerCase and trim the repoPath as that's how they are stored. - // This should be done after splitting the repoPath into username and reponame - // so that username and reponame are not affected. - repoPath = strings.ToLower(username + "/" + reponame) - if !repo.IsValidSSHAccessRepoName(reponame) { return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) } @@ -318,6 +275,11 @@ func runServ(ctx context.Context, c *cli.Command) error { return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error) } + // LowerCase and trim the repoPath as that's how they are stored. + // This should be done after splitting the repoPath into username and reponame + // so that username and reponame are not affected. + repoPath = strings.ToLower(results.OwnerName + "/" + results.RepoName) + // LFS SSH protocol if verb == git.CmdVerbLfsTransfer { token, err := getLFSAuthToken(ctx, lfsVerb, results) diff --git a/routers/private/serv.go b/routers/private/serv.go index b879be0dc2067..a3b78965c8c40 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -11,6 +11,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -108,6 +109,18 @@ func ServCommand(ctx *context.PrivateContext) { results.RepoName = repoName[:len(repoName)-5] } + // Check if there is a user redirect for the requested owner + redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName) + if err == nil { + owner, err := user_model.GetUserByID(ctx, redirectedUserID) + if err == nil { + log.Info("User %s has been redirected to %s", results.OwnerName, owner.Name) + results.OwnerName = owner.Name + } else { + log.Warn("User %s has a redirect to user with ID %d, but no user with this ID could be found. Trying without redirect...", results.OwnerName, redirectedUserID) + } + } + owner, err := user_model.GetUserByName(ctx, results.OwnerName) if err != nil { if user_model.IsErrUserNotExist(err) { @@ -131,6 +144,19 @@ func ServCommand(ctx *context.PrivateContext) { return } + redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName) + if err == nil { + redirectedRepo, err := repo.GetRepositoryByID(ctx, redirectedRepoID) + if err == nil { + log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name) + results.RepoName = redirectedRepo.Name + results.OwnerName = redirectedRepo.OwnerName + owner.ID = redirectedRepo.OwnerID + } else { + log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID) + } + } + // Now get the Repository and set the results section repoExist := true repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName) From 6a87359f22211c162c71185152bb1ecec33b1d7a Mon Sep 17 00:00:00 2001 From: koalajoe Date: Fri, 5 Sep 2025 23:32:48 +0000 Subject: [PATCH 5/7] fix duplicaate import --- routers/private/serv.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/routers/private/serv.go b/routers/private/serv.go index a3b78965c8c40..3dfe4d21da7a5 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -11,7 +11,6 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" - "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -146,7 +145,7 @@ func ServCommand(ctx *context.PrivateContext) { redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName) if err == nil { - redirectedRepo, err := repo.GetRepositoryByID(ctx, redirectedRepoID) + redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID) if err == nil { log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name) results.RepoName = redirectedRepo.Name From 266b27f5733a9927647a678c670e545e4b222d1f Mon Sep 17 00:00:00 2001 From: koalajoe Date: Sat, 6 Sep 2025 11:51:47 +0000 Subject: [PATCH 6/7] Fix TestGitLFSSSH by not cutting away the .git suffix from repo path --- cmd/serv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/serv.go b/cmd/serv.go index 4c9aa04be9e79..60f7fb92ff05d 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -278,7 +278,7 @@ func runServ(ctx context.Context, c *cli.Command) error { // LowerCase and trim the repoPath as that's how they are stored. // This should be done after splitting the repoPath into username and reponame // so that username and reponame are not affected. - repoPath = strings.ToLower(results.OwnerName + "/" + results.RepoName) + repoPath = strings.ToLower(results.OwnerName + "/" + results.RepoName + ".git") // LFS SSH protocol if verb == git.CmdVerbLfsTransfer { From fbf925cbae6e18eaefa29bec76606bd65d32e732 Mon Sep 17 00:00:00 2001 From: koalajoe Date: Sat, 6 Sep 2025 16:10:22 +0000 Subject: [PATCH 7/7] Add test cases --- models/fixtures/user_redirect.yml | 4 +++ tests/integration/git_ssh_redirect_test.go | 42 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/integration/git_ssh_redirect_test.go diff --git a/models/fixtures/user_redirect.yml b/models/fixtures/user_redirect.yml index 8ff79933983eb..c668cb6c3b7b0 100644 --- a/models/fixtures/user_redirect.yml +++ b/models/fixtures/user_redirect.yml @@ -2,3 +2,7 @@ id: 1 lower_name: olduser1 redirect_user_id: 1 +- + id: 2 + lower_name: olduser2 + redirect_user_id: 2 diff --git a/tests/integration/git_ssh_redirect_test.go b/tests/integration/git_ssh_redirect_test.go new file mode 100644 index 0000000000000..5e35ed2a7442f --- /dev/null +++ b/tests/integration/git_ssh_redirect_test.go @@ -0,0 +1,42 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/url" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" +) + +func TestGitSSHRedirect(t *testing.T) { + onGiteaRun(t, testGitSSHRedirect) +} + +func testGitSSHRedirect(t *testing.T, u *url.URL) { + apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + withKeyFile(t, "my-testing-key", func(keyFile string) { + t.Run("CreateUserKey", doAPICreateUserKey(apiTestContext, "test-key", keyFile)) + + testCases := []struct { + testName string + userName string + repoName string + }{ + {"Test untouched", "user2", "repo1"}, + {"Test renamed user", "olduser2", "repo1"}, + {"Test renamed repo", "user2", "oldrepo1"}, + {"Test renamed user and repo", "olduser2", "oldrepo1"}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + cloneURL := createSSHUrl(fmt.Sprintf("%s/%s.git", tc.userName, tc.repoName), u) + t.Run("Clone", doGitClone(t.TempDir(), cloneURL)) + }) + } + }) +}