diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index acbd26cbdf69c15729b86641dfd2b5fa9c794b26..a24273488fb086a2bf0b8b136de5497648446759 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -81,6 +81,7 @@ def mirror_params_attributes
only_protected_branches
keep_divergent_refs
auth_method
+ user
password
ssh_known_hosts
regenerate_ssh_private_key
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index bc162458eae5efd71632ddac71ebd2f16b33cce3..ab039bdead254ed4f7cf8c31575684a903e69970 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -1,5 +1,5 @@
- mirror = f.object
-- auth_options = [[_('Password'), 'password'], [_('SSH public key'), 'ssh_public_key']]
+- auth_options = [[_('Username and Password'), 'password'], [_('SSH public key'), 'ssh_public_key']]
.form-group
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
@@ -9,6 +9,9 @@
= f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
.form-group
- .well-password-auth.collapse.js-well-password-auth
+ = f.label :user, _('Username'), class: 'label-bold'
+ = f.text_field :user, class: 'form-control gl-form-input gl-form-input-xl', value: nil, autocomplete: 'off', required: false, autocorrect: 'off', autocapitalize: 'off', spellcheck: false
+.well-password-auth.collapse.js-well-password-auth
+ .form-group
= f.label :password, _("Password"), class: "label-bold"
= f.password_field :password, class: 'form-control gl-form-input js-mirror-password-field gl-form-input-xl', autocomplete: 'off', data: { qa_selector: 'password_field' }
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 2bd2c7cac4424b06f657543ba1b37f767532dc88..5a8710a64b02f2114e3bfb2276c286bb817e775f 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -4,7 +4,7 @@
= html_escape(_('The repository must be accessible over %{code_open}http://%{code_close},
%{code_open}https://%{code_close}, %{code_open}ssh://%{code_close} or %{code_open}git://%{code_close}.')) % { code_open: ''.html_safe, code_close: ''.html_safe }
%li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: ''.html_safe, code_close: ''.html_safe }
- %li= html_escape(_('Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}.')) % { code_open: ''.html_safe, code_close: ''.html_safe }
+ %li= html_escape(_('Do not include the username in the URL, use the username field below if required: %{code_open}https://gitlab.company.com/group/project.git%{code_close}.')) % { code_open: ''.html_safe, code_close: ''.html_safe }
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
= _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes }
diff --git a/ee/app/controllers/ee/projects/mirrors_controller.rb b/ee/app/controllers/ee/projects/mirrors_controller.rb
index 8e725f0cc87da80518d1f875c2678dcc882eff13..cc6dc2df234f8fb1edab639e2b5546cd0a88b2b4 100644
--- a/ee/app/controllers/ee/projects/mirrors_controller.rb
+++ b/ee/app/controllers/ee/projects/mirrors_controller.rb
@@ -68,6 +68,7 @@ def mirror_params_attributes_ee
import_data_attributes: %i[
id
auth_method
+ user
password
ssh_known_hosts
regenerate_ssh_private_key
diff --git a/ee/spec/features/projects/mirror_spec.rb b/ee/spec/features/projects/mirror_spec.rb
index 53f7b3a2cf8dffc1608ea119977a2f156e4b6372..05e58158f953907b493e797fc49e8f648414f3b5 100644
--- a/ee/spec/features/projects/mirror_spec.rb
+++ b/ee/spec/features/projects/mirror_spec.rb
@@ -108,17 +108,27 @@
end
describe 'password authentication' do
- it 'can be set up' do
+ let(:url) { 'http://example.com' }
+ let(:username) { 'user' }
+ let(:password) { 'foo' }
+ let(:direction) { 'Pull' }
+
+ def add_mirror
visit project_settings_repository_path(project)
click_button('Add new')
page.within('.project-mirror-settings') do
- fill_and_wait_for_mirror_url_javascript('Git repository URL', 'http://user@example.com')
+ fill_and_wait_for_mirror_url_javascript('Git repository URL', url)
- select 'Pull', from: 'Mirror direction'
- fill_in 'Password', with: 'foo'
+ select direction, from: 'Mirror direction'
+ fill_in 'Username', with: username
+ fill_in 'Password', with: password
click_without_sidekiq 'Mirror repository'
end
+ end
+
+ it 'can be set up' do
+ add_mirror
expect(page).to have_content('Mirroring settings were successfully updated')
@@ -128,6 +138,73 @@
expect(project.import_url).to eq('http://user:foo@example.com')
end
+ context 'when username is provided in url' do
+ let(:url) { 'http://urlusername@example.com' }
+
+ it 'will be ignored' do
+ add_mirror
+
+ expect(page).to have_content('Mirroring settings were successfully updated')
+
+ project.reload
+ expect(project.mirror?).to be_truthy
+ expect(import_data.auth_method).to eq('password')
+ expect(project.import_url).to eq('http://user:foo@example.com')
+ end
+ end
+
+ context 'when given special characters' do
+ context 'in the username' do
+ let(:username) { 'u@s+e/r' }
+ let(:password) { 'foo' }
+
+ context 'when adding a pull mirror' do
+ it 'escapes special characters in username' do
+ add_mirror
+
+ project.reload
+ expect(project.import_url).to eq('http://u%40s%2Be%2Fr:foo@example.com')
+ end
+ end
+
+ context 'when adding a push mirror' do
+ let(:direction) { 'Push' }
+
+ it 'escapes special characters in username' do # this one
+ add_mirror
+
+ project.reload
+ expect(project.remote_mirrors.first.url).to eq('http://u%40s%2Be%2Fr:foo@example.com')
+ end
+ end
+ end
+
+ context 'in the password' do
+ let(:username) { 'user' }
+ let(:password) { 'f@o+o/' }
+
+ context 'when adding a pull mirror' do
+ it 'escapes special characters in password' do
+ add_mirror
+
+ project.reload
+ expect(project.import_url).to eq('http://user:f%40o%2Bo%2F@example.com')
+ end
+ end
+
+ context 'when adding a push mirror' do
+ let(:direction) { 'Push' }
+
+ it 'escapes special characters in the password' do # this one
+ add_mirror
+
+ project.reload
+ expect(project.remote_mirrors.first.url).to eq('http://user:f%40o%2Bo%2F@example.com')
+ end
+ end
+ end
+ end
+
it 'can be changed to unauthenticated', quarantine: 'https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/1486' do # rubocop:disable Layout/LineLength
project.update!(import_url: 'http://user:password@example.com')
@@ -153,9 +230,10 @@
click_button('Add new')
page.within('.project-mirror-settings') do
- fill_and_wait_for_mirror_url_javascript('Git repository URL', 'ssh://user@example.com')
+ fill_and_wait_for_mirror_url_javascript('Git repository URL', 'ssh://example.com')
select('Pull', from: 'Mirror direction')
+ fill_in 'Username', with: 'user'
select 'SSH public key', from: 'Authentication method'
# Generates an SSH public key with an asynchronous PUT and displays it
@@ -170,9 +248,10 @@
click_button('Add new')
page.within('.project-mirror-settings') do
- fill_and_wait_for_mirror_url_javascript('Git repository URL', 'http://git@example.com')
+ fill_and_wait_for_mirror_url_javascript('Git repository URL', 'http://example.com')
select('Pull', from: 'Mirror direction')
+ fill_in 'Username', with: 'git'
fill_in 'Password', with: 'test_password'
click_without_sidekiq 'Mirror repository'
end
@@ -187,7 +266,7 @@
end
describe 'SSH public key authentication' do
- let(:ssh_url) { 'ssh://user@example.com' }
+ let(:ssh_url) { 'ssh://example.com' }
it 'can be set up' do
visit project_settings_repository_path(project)
@@ -197,6 +276,7 @@
fill_and_wait_for_mirror_url_javascript('Git repository URL', ssh_url)
select('Pull', from: 'Mirror direction')
+ fill_in 'Username', with: 'user'
select 'SSH public key', from: 'Authentication method'
click_without_sidekiq 'Mirror repository'
@@ -211,6 +291,26 @@
expect(import_data.auth_method).to eq('ssh_public_key')
expect(import_data.password).to be_blank
end
+
+ context 'when no username is provided' do
+ it 'import url has no delimiting colon' do
+ visit project_settings_repository_path(project)
+ click_button('Add new')
+
+ page.within('.project-mirror-settings') do
+ fill_and_wait_for_mirror_url_javascript('Git repository URL', ssh_url)
+
+ select('Pull', from: 'Mirror direction')
+ select 'SSH public key', from: 'Authentication method'
+
+ click_without_sidekiq 'Mirror repository'
+ end
+ project.reload
+ expect(project.username_only_import_url).to eq('ssh://example.com')
+ expect(import_data.auth_method).to eq('ssh_public_key')
+ expect(import_data.password).to be_blank
+ end
+ end
end
describe 'host key management', :use_clean_rails_memory_store_caching do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7501db76a2459415ffb971001939a535aa737a9d..66a352e947ba848ef04b0116a3732ebecf80f1a9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -17242,6 +17242,9 @@ msgstr ""
msgid "Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}"
msgstr ""
+msgid "Do not include the username in the URL, use the username field below if required: %{code_open}https://gitlab.company.com/group/project.git%{code_close}."
+msgstr ""
+
msgid "Do you want to remove this deploy key?"
msgstr ""
@@ -24795,9 +24798,6 @@ msgstr ""
msgid "Include the name of the author of the issue, merge request or comment in the email body. By default, GitLab overrides the email sender's name. Some email servers don't support that option."
msgstr ""
-msgid "Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}."
-msgstr ""
-
msgid "Includes LFS objects. It can be overridden per group, or per project. Set to 0 for no limit."
msgstr ""
@@ -51531,6 +51531,9 @@ msgstr ""
msgid "Username (optional)"
msgstr ""
+msgid "Username and Password"
+msgstr ""
+
msgid "Username is already taken."
msgstr ""