diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 138be33c1c5..0c3a0ab44e3 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -236,6 +236,12 @@ type EditRepoOption struct { MirrorInterval *string `json:"mirror_interval,omitempty"` // enable prune - remove obsolete remote-tracking references when mirroring EnablePrune *bool `json:"enable_prune,omitempty"` + // authentication username for the remote repository (mirrors) + MirrorUsername *string `json:"mirror_username,omitempty"` + // authentication password for the remote repository (mirrors) + MirrorPassword *string `json:"mirror_password,omitempty"` + // authentication token for the remote repository (mirrors) + MirrorToken *string `json:"mirror_token,omitempty"` } // GenerateRepoOption options when creating a repository using a template diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index dc99cf8c162..7431493a3fc 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -257,7 +257,7 @@ func handleRemoteAddrError(ctx *context.APIContext, err error) { addrErr := err.(*git.ErrInvalidCloneAddr) switch { case addrErr.IsURLError: - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, "The provided URL is invalid.") case addrErr.IsPermissionDenied: if addrErr.LocalPath { ctx.APIError(http.StatusUnprocessableEntity, "You are not allowed to import local repositories.") diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index f0428f2f6d6..8b0dc7c863b 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -21,6 +21,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" @@ -37,6 +38,8 @@ import ( "code.gitea.io/gitea/services/convert" feed_service "code.gitea.io/gitea/services/feed" "code.gitea.io/gitea/services/issue" + "code.gitea.io/gitea/services/migrations" + mirror_service "code.gitea.io/gitea/services/mirror" repo_service "code.gitea.io/gitea/services/repository" ) @@ -628,7 +631,11 @@ func Edit(ctx *context.APIContext) { } } - if opts.MirrorInterval != nil || opts.EnablePrune != nil { + if opts.MirrorInterval != nil || + opts.EnablePrune != nil || + opts.MirrorUsername != nil || + opts.MirrorPassword != nil || + opts.MirrorToken != nil { if err := updateMirror(ctx, opts); err != nil { return } @@ -1059,6 +1066,57 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune) } + authUpdateRequested := opts.MirrorPassword != nil || opts.MirrorToken != nil || opts.MirrorUsername != nil + if authUpdateRequested { + remoteURL, err := gitrepo.GitRemoteGetURL(ctx, repo, mirror.GetRemoteName()) + if err != nil { + ctx.APIErrorInternal(err) + return err + } + + authUsername := "" + if opts.MirrorUsername != nil { + authUsername = *opts.MirrorUsername + } else if remoteURL.User != nil { + authUsername = remoteURL.User.Username() + } + + authPassword := "" + authToken := "" + if opts.MirrorPassword != nil { + authPassword = *opts.MirrorPassword + } + if opts.MirrorToken != nil { + authToken = *opts.MirrorToken + } + + if opts.MirrorPassword == nil && opts.MirrorToken == nil && remoteURL.User != nil && (authUsername == "" || authUsername == remoteURL.User.Username()) { + authPassword, _ = remoteURL.User.Password() + } + + if authToken != "" { + authPassword = authToken + } + + composedAddress, err := git.ParseRemoteAddr(repo.OriginalURL, authUsername, authPassword) + if err == nil { + err = migrations.IsMigrateURLAllowed(composedAddress, ctx.Doer) + } + if err != nil { + handleRemoteAddrError(ctx, err) + return err + } + + if err := mirror_service.UpdateAddress(ctx, mirror, composedAddress); err != nil { + ctx.APIErrorInternal(err) + return err + } + + if sanitized, err := util.SanitizeURL(repo.OriginalURL); err == nil { + mirror.RemoteAddress = sanitized + } + } + // finally update the mirror in the DB if err := repo_model.UpdateMirror(ctx, mirror); err != nil { log.Error("Failed to Set Mirror Interval: %s", err) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 703d0022504..816fd91cd8b 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -265,8 +265,13 @@ func handleSettingsPostMirror(ctx *context.Context) { handleSettingRemoteAddrError(ctx, err, form) return } - if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() { - form.MirrorPassword, _ = u.User.Password() + if u.User != nil { + if form.MirrorUsername == "" { + form.MirrorUsername = u.User.Username() + } + if form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() { + form.MirrorPassword, _ = u.User.Password() + } } address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword) diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go index 4c65b696c59..154d01fda40 100644 --- a/routers/web/repo/setting/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -13,15 +13,18 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/forms" + mirror_service "code.gitea.io/gitea/services/mirror" repo_service "code.gitea.io/gitea/services/repository" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAddReadOnlyDeployKey(t *testing.T) { @@ -386,3 +389,45 @@ func TestDeleteTeam(t *testing.T) { assert.False(t, repo_service.HasRepository(t.Context(), team, re.ID)) } + +func TestHandleSettingsPostMirrorPreservesExistingUsername(t *testing.T) { + defer test.MockVariableValue(&setting.Mirror.Enabled, true)() + + unittest.PrepareTestEnv(t) + + // Use the existing fixture mirror repo (org3/repo5) which has a git repo on disk. + mirrorRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5}) + mirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: 5}) + + require.NoError(t, mirror_service.UpdateAddress(t.Context(), mirror, "https://existing-user:existing-password@example.com/user2/repo1.git")) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + ctx, _ := contexttest.MockContext(t, mirrorRepo.Link()+"/settings") + contexttest.LoadUser(t, ctx, user.ID) + contexttest.LoadRepo(t, ctx, mirrorRepo.ID) + + web.SetForm(ctx, &forms.RepoSettingForm{ + Interval: "8h", + MirrorAddress: "https://example.com/user2/repo1.git", + MirrorPassword: "updated-password", + }) + + handleSettingsPostMirror(ctx) + + assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) + + updatedMirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID}) + assert.Equal(t, "https://example.com/user2/repo1.git", updatedMirror.RemoteAddress) + + updatedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: mirrorRepo.ID}) + assert.Equal(t, "https://example.com/user2/repo1.git", updatedRepo.OriginalURL) + + remoteURL, err := gitrepo.GitRemoteGetURL(t.Context(), updatedRepo, updatedMirror.GetRemoteName()) + require.NoError(t, err) + require.NotNil(t, remoteURL.User) + assert.Equal(t, "existing-user", remoteURL.User.Username()) + password, ok := remoteURL.User.Password() + require.True(t, ok) + assert.Equal(t, "updated-password", password) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index eac1ae67247..26d45940f25 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -25510,6 +25510,21 @@ "type": "string", "x-go-name": "MirrorInterval" }, + "mirror_password": { + "description": "authentication password for the remote repository (mirrors)", + "type": "string", + "x-go-name": "MirrorPassword" + }, + "mirror_token": { + "description": "authentication token for the remote repository (mirrors)", + "type": "string", + "x-go-name": "MirrorToken" + }, + "mirror_username": { + "description": "authentication username for the remote repository (mirrors)", + "type": "string", + "x-go-name": "MirrorUsername" + }, "name": { "description": "name of the repository", "type": "string", diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index a45def2ddfe..33adff75e0d 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -5738,6 +5738,21 @@ "type": "string", "x-go-name": "MirrorInterval" }, + "mirror_password": { + "description": "authentication password for the remote repository (mirrors)", + "type": "string", + "x-go-name": "MirrorPassword" + }, + "mirror_token": { + "description": "authentication token for the remote repository (mirrors)", + "type": "string", + "x-go-name": "MirrorToken" + }, + "mirror_username": { + "description": "authentication username for the remote repository (mirrors)", + "type": "string", + "x-go-name": "MirrorUsername" + }, "name": { "description": "name of the repository", "type": "string", diff --git a/tests/integration/api_repo_edit_test.go b/tests/integration/api_repo_edit_test.go index 46bcccc31ee..e37b214fa88 100644 --- a/tests/integration/api_repo_edit_test.go +++ b/tests/integration/api_repo_edit_test.go @@ -15,9 +15,12 @@ import ( unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/gitrepo" api "code.gitea.io/gitea/modules/structs" + mirror_service "code.gitea.io/gitea/services/mirror" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // getRepoEditOptionFromRepo gets the options for an existing repo exactly as is @@ -432,5 +435,56 @@ func TestAPIRepoEdit(t *testing.T) { DefaultDeleteBranchAfterMerge: &bFalse, }).AddTokenAuth(token2) _ = MakeRequest(t, req, http.StatusOK) + + // Test updating mirror password without changing the existing username + ctx := t.Context() + mirrorRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5}) + mirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: 5}) + newPassword := "updated-password" + + require.NoError(t, mirror_service.UpdateAddress(ctx, mirror, "https://existing-user:existing-password@example.com/user2/repo1.git")) + + req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s", mirrorRepo.OwnerName, mirrorRepo.Name), &api.EditRepoOption{ + MirrorPassword: &newPassword, + }).AddTokenAuth(token2) + MakeRequest(t, req, http.StatusOK) + + updatedMirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID}) + assert.Equal(t, "https://example.com/user2/repo1.git", updatedMirror.RemoteAddress) + + updatedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: mirrorRepo.ID}) + assert.Equal(t, "https://example.com/user2/repo1.git", updatedRepo.OriginalURL) + + remoteURL, err := gitrepo.GitRemoteGetURL(ctx, updatedRepo, updatedMirror.GetRemoteName()) + require.NoError(t, err) + require.NotNil(t, remoteURL.User) + assert.Equal(t, "existing-user", remoteURL.User.Username()) + password, ok := remoteURL.User.Password() + require.True(t, ok) + assert.Equal(t, newPassword, password) + + // Test updating mirror token without guessing a username + token := "mirror-token-value" + + require.NoError(t, mirror_service.UpdateAddress(ctx, mirror, "https://example.com/user2/repo1.git")) + + req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s", mirrorRepo.OwnerName, mirrorRepo.Name), &api.EditRepoOption{ + MirrorToken: &token, + }).AddTokenAuth(token2) + MakeRequest(t, req, http.StatusOK) + + updatedMirror = unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID}) + assert.Equal(t, "https://example.com/user2/repo1.git", updatedMirror.RemoteAddress) + + updatedRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: mirrorRepo.ID}) + assert.Equal(t, "https://example.com/user2/repo1.git", updatedRepo.OriginalURL) + + remoteURL, err = gitrepo.GitRemoteGetURL(ctx, updatedRepo, updatedMirror.GetRemoteName()) + require.NoError(t, err) + require.NotNil(t, remoteURL.User) + assert.Empty(t, remoteURL.User.Username()) + password, ok = remoteURL.User.Password() + require.True(t, ok) + assert.Equal(t, token, password) }) }