Class: SshHostKey

Inherits:
Object
  • Object
show all
Includes:
ReactiveCaching
Defined in:
app/models/ssh_host_key.rb

Overview

Detected SSH host keys are transiently stored in Redis

Defined Under Namespace

Classes: Fingerprint

Constant Summary

Constants included from ReactiveCaching

ReactiveCaching::ExceededReactiveCacheLimit, ReactiveCaching::InvalidateReactiveCache, ReactiveCaching::WORK_TYPE

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project:, url:, compare_host_keys: nil) ⇒ SshHostKey

Returns a new instance of SshHostKey.



59
60
61
62
63
# File 'app/models/ssh_host_key.rb', line 59

def initialize(project:, url:, compare_host_keys: nil)
  @project = project
  @url, @ip = normalize_url(url)
  @compare_host_keys = compare_host_keys
end

Instance Attribute Details

#compare_host_keysObject (readonly)

Returns the value of attribute compare_host_keys.



57
58
59
# File 'app/models/ssh_host_key.rb', line 57

def compare_host_keys
  @compare_host_keys
end

#ipObject (readonly)

Returns the value of attribute ip.



57
58
59
# File 'app/models/ssh_host_key.rb', line 57

def ip
  @ip
end

#projectObject (readonly)

Returns the value of attribute project.



57
58
59
# File 'app/models/ssh_host_key.rb', line 57

def project
  @project
end

#urlObject (readonly)

Returns the value of attribute url.



57
58
59
# File 'app/models/ssh_host_key.rb', line 57

def url
  @url
end

Class Method Details

.find_by(opts = {}) ⇒ Object



37
38
39
40
41
42
43
44
45
# File 'app/models/ssh_host_key.rb', line 37

def self.find_by(opts = {})
  opts = HashWithIndifferentAccess.new(opts)
  return unless opts.key?(:id)

  project_id, url = opts[:id].split(':', 2)
  project = Project.find_by(id: project_id)

  project.presence && new(project: project, url: url)
end

.fingerprint_host_keys(data) ⇒ Object



47
48
49
50
51
52
53
54
55
# File 'app/models/ssh_host_key.rb', line 47

def self.fingerprint_host_keys(data)
  return [] unless data.is_a?(String)

  data
    .each_line
    .each_with_index
    .map { |line, index| Fingerprint.new(line, index: index) }
    .select(&:valid?)
end

.primary_keyObject

Needed for reactive caching



66
67
68
# File 'app/models/ssh_host_key.rb', line 66

def self.primary_key
  :id
end

Instance Method Details

#as_jsonObject



74
75
76
77
78
79
80
# File 'app/models/ssh_host_key.rb', line 74

def as_json(*)
  {
    host_keys_changed: host_keys_changed?,
    fingerprints: fingerprints,
    known_hosts: known_hosts
  }
end

#calculate_reactive_cacheObject



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/ssh_host_key.rb', line 100

def calculate_reactive_cache
  input = [ip, url.hostname].compact.join(' ')

  known_hosts, errors, status =
    Open3.popen3({}, *%W[ssh-keyscan -T 5 -p #{url.port} -f-]) do |stdin, stdout, stderr, wait_thr|
      stdin.puts(input)
      stdin.close

      [
        cleanup(stdout.read),
        cleanup(stderr.read),
        wait_thr.value
      ]
    end

  # ssh-keyscan returns an exit code 0 in several error conditions, such as an
  # unknown hostname, so check both STDERR and the exit code
  if status.success? && !errors.present?
    { known_hosts: known_hosts }
  else
    Gitlab::AppLogger.debug("Failed to detect SSH host keys for #{id}: #{errors}")

    { error: 'Failed to detect SSH host keys' }
  end
end

#errorObject



96
97
98
# File 'app/models/ssh_host_key.rb', line 96

def error
  with_reactive_cache { |data| data[:error] }
end

#fingerprintsObject



86
87
88
# File 'app/models/ssh_host_key.rb', line 86

def fingerprints
  @fingerprints ||= self.class.fingerprint_host_keys(known_hosts)
end

#host_keys_changed?Boolean

Returns true if the known_hosts data differs from the version passed in at initialization as ‘compare_host_keys`. Comments, ordering, etc, is ignored

Returns:

  • (Boolean)


92
93
94
# File 'app/models/ssh_host_key.rb', line 92

def host_keys_changed?
  cleanup(known_hosts) != cleanup(compare_host_keys)
end

#idObject



70
71
72
# File 'app/models/ssh_host_key.rb', line 70

def id
  [project.id, url].join(':')
end

#known_hostsObject



82
83
84
# File 'app/models/ssh_host_key.rb', line 82

def known_hosts
  with_reactive_cache { |data| data[:known_hosts] }
end