From d530ea2b73c79511c2896b673d20e1612e0f0dc9 Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Wed, 17 Dec 2025 17:18:39 +0100 Subject: [PATCH 01/10] feat: Add host_keys setter to RemoteMirror model Convert API host_keys parameter to ssh_known_hosts format for storage. Changelog: changed --- app/models/remote_mirror.rb | 7 +++++++ lib/api/remote_mirrors.rb | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 1638a9307b482c..b3399706b004c0 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -253,6 +253,13 @@ def store_credentials self.credentials = self.credentials end + def host_keys=(fingerprints) + if fingerprints.present? + self.ssh_known_hosts = fingerprints.map { |fp| fp[:fingerprint_sha256] }.join("\n") + end +end + + # The remote URL omits any password if SSH public-key authentication is in use def remote_url return url unless ssh_key_auth? && password.present? diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb index 4599f9c2ce6363..d38276e6c66c63 100644 --- a/lib/api/remote_mirrors.rb +++ b/lib/api/remote_mirrors.rb @@ -94,6 +94,8 @@ def find_remote_mirror optional :enabled, type: Boolean, desc: 'Determines if the mirror is enabled', documentation: { example: false } optional :auth_method, type: String, desc: 'Determines the mirror authentication method', values: %w[ssh_public_key password] + optional :host_keys, type: Array, desc: 'Array of SSH host key fingerprints for the remote mirror', + documentation: { example: [{ fingerprint_sha256: 'SHA256:...' }] } optional :keep_divergent_refs, type: Boolean, desc: 'Determines if divergent refs are kept on the target', documentation: { example: false } use :mirror_branches_setting @@ -127,6 +129,8 @@ def find_remote_mirror requires :mirror_id, type: String, desc: 'The ID of a remote mirror' optional :enabled, type: Boolean, desc: 'Determines if the mirror is enabled', documentation: { example: true } optional :auth_method, type: String, desc: 'Determines the mirror authentication method' + optional :host_keys, type: Array, desc: 'Array of SSH host key fingerprints for the remote mirror', + documentation: { example: [{ fingerprint_sha256: 'SHA256:...' }] } optional :keep_divergent_refs, type: Boolean, desc: 'Determines if divergent refs are kept on the target', documentation: { example: false } use :mirror_branches_setting -- GitLab From 02aaf16abfa1bea4b65f52be1384f0f388f17c31 Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Wed, 17 Dec 2025 18:23:18 +0100 Subject: [PATCH 02/10] Attempt 2 --- app/models/remote_mirror.rb | 7 ------- app/services/remote_mirrors/update_service.rb | 4 +++- lib/api/remote_mirrors.rb | 8 ++++---- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index b3399706b004c0..1638a9307b482c 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -253,13 +253,6 @@ def store_credentials self.credentials = self.credentials end - def host_keys=(fingerprints) - if fingerprints.present? - self.ssh_known_hosts = fingerprints.map { |fp| fp[:fingerprint_sha256] }.join("\n") - end -end - - # The remote URL omits any password if SSH public-key authentication is in use def remote_url return url unless ssh_key_auth? && password.present? diff --git a/app/services/remote_mirrors/update_service.rb b/app/services/remote_mirrors/update_service.rb index dc7de04576c85c..814ecb53c02dea 100644 --- a/app/services/remote_mirrors/update_service.rb +++ b/app/services/remote_mirrors/update_service.rb @@ -7,7 +7,9 @@ def execute(remote_mirror) return ServiceResponse.error(message: _('Remote mirror is missing')) unless remote_mirror return ServiceResponse.error(message: _('Project mismatch')) unless remote_mirror.project == project - if remote_mirror.update(allowed_attributes) + remote_mirror.assign_attributes(allowed_attributes) + + if remote_mirror.save ServiceResponse.success(payload: { remote_mirror: remote_mirror }) else ServiceResponse.error(message: remote_mirror.errors) diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb index d38276e6c66c63..2b21235bf14b90 100644 --- a/lib/api/remote_mirrors.rb +++ b/lib/api/remote_mirrors.rb @@ -94,8 +94,8 @@ def find_remote_mirror optional :enabled, type: Boolean, desc: 'Determines if the mirror is enabled', documentation: { example: false } optional :auth_method, type: String, desc: 'Determines the mirror authentication method', values: %w[ssh_public_key password] - optional :host_keys, type: Array, desc: 'Array of SSH host key fingerprints for the remote mirror', - documentation: { example: [{ fingerprint_sha256: 'SHA256:...' }] } + optional :ssh_known_hosts, type: String, desc: 'SSH known hosts for the remote mirror', + documentation: { example: 'ssh-rsa AAAAB3NzaC1yc2E...' } optional :keep_divergent_refs, type: Boolean, desc: 'Determines if divergent refs are kept on the target', documentation: { example: false } use :mirror_branches_setting @@ -129,8 +129,8 @@ def find_remote_mirror requires :mirror_id, type: String, desc: 'The ID of a remote mirror' optional :enabled, type: Boolean, desc: 'Determines if the mirror is enabled', documentation: { example: true } optional :auth_method, type: String, desc: 'Determines the mirror authentication method' - optional :host_keys, type: Array, desc: 'Array of SSH host key fingerprints for the remote mirror', - documentation: { example: [{ fingerprint_sha256: 'SHA256:...' }] } + optional :ssh_known_hosts, type: String, desc: 'SSH known hosts for the remote mirror', + documentation: { example: 'ssh-rsa AAAAB3NzaC1yc2E...' } optional :keep_divergent_refs, type: Boolean, desc: 'Determines if divergent refs are kept on the target', documentation: { example: false } use :mirror_branches_setting -- GitLab From 42bec1a6bce4d1b50ae7bd5a3d6e057bfd68157b Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Thu, 18 Dec 2025 17:50:29 +0100 Subject: [PATCH 03/10] MVC - Accepts an array of actual host keys --- app/models/remote_mirrors/attributes.rb | 7 ++++++- app/serializers/remote_mirror_entity.rb | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/remote_mirrors/attributes.rb b/app/models/remote_mirrors/attributes.rb index 250dc943803945..cc67e1555795a6 100644 --- a/app/models/remote_mirrors/attributes.rb +++ b/app/models/remote_mirrors/attributes.rb @@ -19,7 +19,12 @@ def initialize(attrs) end def allowed - attrs.slice(*keys) + allowed_attrs = attrs.slice(*keys) + + # Convert ssh_known_hosts array to newline-separated string + if allowed_attrs[:ssh_known_hosts].is_a?(Array) + allowed_attrs[:ssh_known_hosts] = allowed_attrs[:ssh_known_hosts].join("\n") + end end def keys diff --git a/app/serializers/remote_mirror_entity.rb b/app/serializers/remote_mirror_entity.rb index 7eddb3fef4a43c..e3d3e632bfc5b7 100644 --- a/app/serializers/remote_mirror_entity.rb +++ b/app/serializers/remote_mirror_entity.rb @@ -12,6 +12,10 @@ class RemoteMirrorEntity < Grape::Entity expose :ssh_known_hosts_fingerprints do |remote_mirror| remote_mirror.ssh_known_hosts_fingerprints.as_json end + + expose :host_keys do |remote_mirror| + remote_mirror.ssh_known_hosts_fingerprints.as_json + end end RemoteMirrorEntity.prepend_mod -- GitLab From a1b428fe8c58fb9f23248a139e1d88b7efadf273 Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Thu, 18 Dec 2025 17:53:38 +0100 Subject: [PATCH 04/10] Accept string arrays --- lib/api/remote_mirrors.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb index 2b21235bf14b90..34f9eff1afca85 100644 --- a/lib/api/remote_mirrors.rb +++ b/lib/api/remote_mirrors.rb @@ -94,8 +94,8 @@ def find_remote_mirror optional :enabled, type: Boolean, desc: 'Determines if the mirror is enabled', documentation: { example: false } optional :auth_method, type: String, desc: 'Determines the mirror authentication method', values: %w[ssh_public_key password] - optional :ssh_known_hosts, type: String, desc: 'SSH known hosts for the remote mirror', - documentation: { example: 'ssh-rsa AAAAB3NzaC1yc2E...' } + optional :ssh_known_hosts, type: Array[String], desc: 'Array of SSH public keys for the remote mirror', + documentation: { example: ['ssh-rsa AAAAB3NzaC1yc2E...', 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...'] } optional :keep_divergent_refs, type: Boolean, desc: 'Determines if divergent refs are kept on the target', documentation: { example: false } use :mirror_branches_setting @@ -129,8 +129,8 @@ def find_remote_mirror requires :mirror_id, type: String, desc: 'The ID of a remote mirror' optional :enabled, type: Boolean, desc: 'Determines if the mirror is enabled', documentation: { example: true } optional :auth_method, type: String, desc: 'Determines the mirror authentication method' - optional :ssh_known_hosts, type: String, desc: 'SSH known hosts for the remote mirror', - documentation: { example: 'ssh-rsa AAAAB3NzaC1yc2E...' } + optional :ssh_known_hosts, type: Array[String], desc: 'Array of SSH public keys for the remote mirror', + documentation: { example: ['ssh-rsa AAAAB3NzaC1yc2E...', 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...'] } optional :keep_divergent_refs, type: Boolean, desc: 'Determines if divergent refs are kept on the target', documentation: { example: false } use :mirror_branches_setting -- GitLab From 36ed4763a881dab54a462289577594ce8cfe948d Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Thu, 18 Dec 2025 19:05:39 +0100 Subject: [PATCH 05/10] Auto-detect SSH host keys when creating remote mirrors Automatically detect and store SSH host keys from the remote server when creating a mirror with an SSH URL and no explicit ssh_known_hosts parameter. This eliminates the need for users to manually provide host keys during mirror creation, improving the user experience while maintaining backward compatibility with explicit key provision. --- app/services/remote_mirrors/create_service.rb | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/app/services/remote_mirrors/create_service.rb b/app/services/remote_mirrors/create_service.rb index 8819cdfc0fb76f..0e31052d70a1ff 100644 --- a/app/services/remote_mirrors/create_service.rb +++ b/app/services/remote_mirrors/create_service.rb @@ -5,9 +5,15 @@ class CreateService < BaseService def execute return ServiceResponse.error(message: _('Access Denied')) unless allowed? - remote_mirror = project.remote_mirrors.create(allowed_attributes) + remote_mirror = project.remote_mirrors.build + remote_mirror.assign_attributes(allowed_attributes) - if remote_mirror.persisted? + # Auto-detect host keys if ssh_known_hosts is empty/omitted and URL is SSH + if remote_mirror.ssh_mirror_url? && remote_mirror.ssh_known_hosts.blank? + detect_and_set_host_keys(remote_mirror) + end + + if remote_mirror.save ServiceResponse.success(payload: { remote_mirror: remote_mirror }) else ServiceResponse.error(message: remote_mirror.errors) @@ -16,6 +22,18 @@ def execute private + def detect_and_set_host_keys(remote_mirror) + lookup = ::SshHostKey.new(project: project, url: remote_mirror.url) + remote_mirror.ssh_known_hosts = lookup.known_hosts if lookup.known_hosts.present? + rescue StandardError => e + Gitlab::AppJsonLogger.warn( + message: 'Failed to auto-detect SSH host keys', + error: e.message, + project_id: project.id, + url: remote_mirror.url + ) + end + def allowed_attributes RemoteMirrors::Attributes.new(params).allowed end -- GitLab From 86d5bb557d6e46993269e7f18f453ac190a5ee4a Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Thu, 18 Dec 2025 19:18:30 +0100 Subject: [PATCH 06/10] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: GitLab Duo --- app/models/remote_mirrors/attributes.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/remote_mirrors/attributes.rb b/app/models/remote_mirrors/attributes.rb index cc67e1555795a6..22aad673490d74 100644 --- a/app/models/remote_mirrors/attributes.rb +++ b/app/models/remote_mirrors/attributes.rb @@ -23,7 +23,9 @@ def allowed # Convert ssh_known_hosts array to newline-separated string if allowed_attrs[:ssh_known_hosts].is_a?(Array) - allowed_attrs[:ssh_known_hosts] = allowed_attrs[:ssh_known_hosts].join("\n") + # Filter out nil/empty values and validate format + valid_keys = allowed_attrs[:ssh_known_hosts].compact.reject(&:blank?) + allowed_attrs[:ssh_known_hosts] = valid_keys.join("\n") end end -- GitLab From c5cea4ba8488524c270f703dac26f84c06335546 Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Thu, 18 Dec 2025 19:27:05 +0100 Subject: [PATCH 07/10] docs: Add SSH host keys documentation for remote mirror API Document ssh_known_hosts parameter support with auto-detection behavior and usage examples for creating and updating remote mirrors." --- doc/api/remote_mirrors.md | 82 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/doc/api/remote_mirrors.md b/doc/api/remote_mirrors.md index 9b02e780adb257..6aaec44cd9051e 100644 --- a/doc/api/remote_mirrors.md +++ b/doc/api/remote_mirrors.md @@ -294,6 +294,88 @@ Example response: } ``` +## SSH Host Keys + +When creating a remote mirror with an SSH URL, GitLab can automatically detect the remote server's SSH host keys. This matches the behavior of the web interface where "Detect host keys" is the default option. + +### Auto-detection (Recommended) + +When creating a mirror with an SSH URL and no explicit `ssh_known_hosts`, GitLab automatically detects and stores the host keys: + +```shell +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --header "Content-Type: application/json" \ + --data '{ + "url": "ssh://git@gitlab.com/user/repo.git", + "auth_method": "ssh_public_key", + "enabled": true + }' \ + --url "https://gitlab.example.com/api/v4/projects/42/remote_mirrors" +``` + +Response includes auto-detected host keys: + +```json +{ + "id": 101486, + "url": "ssh://git@gitlab.com/user/repo.git", + "auth_method": "ssh_public_key", + "host_keys": [ + { + "fingerprint_sha256": "SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8" + }, + { + "fingerprint_sha256": "SHA256:HbW3g8zUjNSksFbqTiUWPWg2Bq1x8xdGUrliXFzSnUw" + }, + { + "fingerprint_sha256": "SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM" + } + ] +} +``` + +### Explicit Host Keys + +You can also provide SSH public keys explicitly when creating or updating a mirror: + +```shell +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --header "Content-Type: application/json" \ + --data '{ + "url": "ssh://git@gitlab.com/user/repo.git", + "auth_method": "ssh_public_key", + "ssh_known_hosts": [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9", + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf" + ], + "enabled": true + }' \ + --url "https://gitlab.example.com/api/v4/projects/42/remote_mirrors" +``` + +The API derives SHA256 fingerprints from the provided keys and returns them as `host_keys`. + +### Updating Host Keys + +To update the host keys for an existing mirror: + +```shell +curl --request PUT \ + --header "PRIVATE-TOKEN: " \ + --header "Content-Type: application/json" \ + --data '{ + "ssh_known_hosts": [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9", + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf" + ] + }' \ + --url "https://gitlab.example.com/api/v4/projects/42/remote_mirrors/101486" +``` + ## Update a remote mirror's attributes {{< history >}} -- GitLab From 6b2ae6c042a8744e4570217cdb516cfed1f1056e Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Thu, 18 Dec 2025 19:36:21 +0100 Subject: [PATCH 08/10] Add SSH host key format validation Validate ssh_known_hosts using existing SshHostKey.fingerprint_host_keys() to ensure only valid SSH public keys are stored. Invalid keys are rejected with a clear error message. --- app/models/remote_mirror.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 1638a9307b482c..b670c9d4c5db4c 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -24,6 +24,7 @@ class RemoteMirror < ApplicationRecord validates :project, presence: true validates :url, presence: true, public_url: { schemes: Project::VALID_MIRROR_PROTOCOLS, allow_blank: true, enforce_user: true } validates :only_protected_branches, inclusion: { in: [true, false], message: :blank } + validate :validate_ssh_known_hosts_format before_validation :store_credentials after_update :reset_fields, if: :saved_change_to_mirror_url? @@ -253,6 +254,20 @@ def store_credentials self.credentials = self.credentials end + def validate_ssh_known_hosts_format + return if ssh_known_hosts.blank? + + fingerprints = ::SshHostKey.fingerprint_host_keys(ssh_known_hosts) + + # Count valid lines vs total lines + total_lines = ssh_known_hosts.split("\n").reject(&:blank?).count + valid_count = fingerprints.count + + if valid_count < total_lines + errors.add(:ssh_known_hosts, _('contains invalid SSH public key format')) + end +end + # The remote URL omits any password if SSH public-key authentication is in use def remote_url return url unless ssh_key_auth? && password.present? -- GitLab From 92a944a42f887f7ce9c326c41993b01f7090c89f Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Thu, 18 Dec 2025 19:43:47 +0100 Subject: [PATCH 09/10] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: GitLab Duo --- app/models/remote_mirror.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index b670c9d4c5db4c..3b6f0984d84e1c 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -255,7 +255,7 @@ def store_credentials end def validate_ssh_known_hosts_format - return if ssh_known_hosts.blank? + return if ssh_known_hosts.blank? fingerprints = ::SshHostKey.fingerprint_host_keys(ssh_known_hosts) -- GitLab From 7178f5fc4a26576905186c1c4f989df57316c39c Mon Sep 17 00:00:00 2001 From: Niklas Janz Date: Thu, 18 Dec 2025 19:47:39 +0100 Subject: [PATCH 10/10] Add unit tests for changes --- spec/models/remote_mirror_spec.rb | 23 +++++++++ spec/requests/api/remote_mirrors_spec.rb | 59 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 2e79daf7a28106..4c7768243c7862 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -11,6 +11,29 @@ it { is_expected.to allow_value(true, false).for(:only_protected_branches) } it { is_expected.not_to allow_value(nil).for(:only_protected_branches) } it { is_expected.to validate_presence_of(:project) } + + describe 'ssh_known_hosts validation' do + it 'accepts valid SSH public keys' do + ssh_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9' + mirror = build(:remote_mirror, ssh_known_hosts: ssh_key) + + expect(mirror).to be_valid + end + + it 'rejects invalid SSH key format' do + mirror = build(:remote_mirror, ssh_known_hosts: 'invalid-key-format') + + expect(mirror).not_to be_valid + expect(mirror.errors[:ssh_known_hosts]).to match_array(['contains invalid SSH public key format']) + end + + it 'accepts multiple valid SSH keys' do + ssh_keys = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf" + mirror = build(:remote_mirror, ssh_known_hosts: ssh_keys) + + expect(mirror).to be_valid + end + end end describe 'URL validation' do diff --git a/spec/requests/api/remote_mirrors_spec.rb b/spec/requests/api/remote_mirrors_spec.rb index d14e8ee62a797b..864dfe597deab5 100644 --- a/spec/requests/api/remote_mirrors_spec.rb +++ b/spec/requests/api/remote_mirrors_spec.rb @@ -193,6 +193,39 @@ expect(json_response['message']['only_protected_branches']).to match_array(["can't be blank"]) end end + + context 'with ssh_known_hosts parameter' do + let(:ssh_key_rsa) { 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9' } + let(:ssh_key_ed25519) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf' } + + it 'creates a remote mirror with valid ssh_known_hosts' do + project.add_maintainer(user) + + post api(route, user), params: { + url: 'ssh://git@example.com/user/repo.git', + auth_method: 'ssh_public_key', + ssh_known_hosts: [ssh_key_rsa, ssh_key_ed25519], + enabled: true + } + + expect(response).to have_gitlab_http_status(:success) + expect(json_response['host_keys']).to have_length(2) + end + + it 'rejects invalid ssh_known_hosts' do + project.add_maintainer(user) + + post api(route, user), params: { + url: 'ssh://git@example.com/user/repo.git', + auth_method: 'ssh_public_key', + ssh_known_hosts: ['invalid-key-format'], + enabled: true + } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['ssh_known_hosts']).to match_array(['contains invalid SSH public key format']) + end + end end describe 'PUT /projects/:id/remote_mirrors/:mirror_id' do @@ -245,6 +278,32 @@ expect(json_response['message']['only_protected_branches']).to match_array(["can't be blank"]) end end + + context 'with ssh_known_hosts parameter' do + let(:ssh_key_rsa) { 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9' } + + it 'updates ssh_known_hosts' do + project.add_maintainer(user) + + put api(route, user), params: { + ssh_known_hosts: [ssh_key_rsa] + } + + expect(response).to have_gitlab_http_status(:success) + expect(json_response['host_keys']).to have_length(1) + end + + it 'rejects invalid ssh_known_hosts' do + project.add_maintainer(user) + + put api(route, user), params: { + ssh_known_hosts: ['not-a-valid-key'] + } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['ssh_known_hosts']).to match_array(['contains invalid SSH public key format']) + end + end end describe 'DELETE /projects/:id/remote_mirrors/:mirror_id' do -- GitLab