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.


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

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.


49
50
51
# File 'app/models/ssh_host_key.rb', line 49

def compare_host_keys
  @compare_host_keys
end

#ipObject (readonly)

Returns the value of attribute ip.


49
50
51
# File 'app/models/ssh_host_key.rb', line 49

def ip
  @ip
end

#projectObject (readonly)

Returns the value of attribute project.


49
50
51
# File 'app/models/ssh_host_key.rb', line 49

def project
  @project
end

#urlObject (readonly)

Returns the value of attribute url.


49
50
51
# File 'app/models/ssh_host_key.rb', line 49

def url
  @url
end

Class Method Details

.find_by(opts = {}) ⇒ Object


29
30
31
32
33
34
35
36
37
# File 'app/models/ssh_host_key.rb', line 29

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


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

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


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

def self.primary_key
  :id
end

Instance Method Details

#as_jsonObject


66
67
68
69
70
71
72
# File 'app/models/ssh_host_key.rb', line 66

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

#calculate_reactive_cacheObject


92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'app/models/ssh_host_key.rb', line 92

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


88
89
90
# File 'app/models/ssh_host_key.rb', line 88

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

#fingerprintsObject


78
79
80
# File 'app/models/ssh_host_key.rb', line 78

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)

84
85
86
# File 'app/models/ssh_host_key.rb', line 84

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

#idObject


62
63
64
# File 'app/models/ssh_host_key.rb', line 62

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

#known_hostsObject


74
75
76
# File 'app/models/ssh_host_key.rb', line 74

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