Class: R10K::Git::Rugged::Credentials Private

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/r10k/git/rugged/credentials.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Generate credentials for secured remote connections.

Constant Summary

Constants included from Logging

Logging::LOG_LEVELS

Instance Method Summary collapse

Methods included from Logging

debug_formatter, default_formatter, default_outputter, #logger, #logger_name, parse_level

Constructor Details

#initialize(repository) ⇒ Credentials

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Credentials.

Parameters:



17
18
19
20
# File 'lib/r10k/git/rugged/credentials.rb', line 17

def initialize(repository)
  @repository = repository
  @called = 0
end

Instance Method Details

#call(url, username_from_url, allowed_types) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/r10k/git/rugged/credentials.rb', line 22

def call(url, username_from_url, allowed_types)
  @called += 1

  # Break out of infinite HTTP auth retry loop introduced in libgit2/rugged 0.24.0, libssh
  # auth seems to already abort after ~50 attempts.
  if @called > 50
    raise R10K::Git::GitError.new(_("Authentication failed for Git remote %{url}.") % {url: url.inspect} )
  end

  if allowed_types.include?(:ssh_key)
    get_ssh_key_credentials(url, username_from_url)
  elsif allowed_types.include?(:plaintext)
    get_plaintext_credentials(url, username_from_url)
  else
    get_default_credentials(url, username_from_url)
  end
end

#extract_token(token_path, url) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/r10k/git/rugged/credentials.rb', line 99

def extract_token(token_path, url)
  if token_path == '-'
    token = $stdin.read.strip
    logger.debug2 _("Using OAuth token from stdin for URL %{url}") % { url: url }
  elsif File.readable?(token_path)
    token = File.read(token_path).strip
    logger.debug2 _("Using OAuth token from %{token_path} for URL %{url}") % { token_path: token_path, url: url }
  else
    raise R10K::Git::GitError, _("%{path} is missing or unreadable, cannot load OAuth token") % { path: token_path }
  end

  unless valid_token?(token)
    raise R10K::Git::GitError, _("Supplied OAuth token contains invalid characters.")
  end

  token
end

#get_default_credentials(url, username_from_url) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



124
125
126
# File 'lib/r10k/git/rugged/credentials.rb', line 124

def get_default_credentials(url, username_from_url)
  Rugged::Credentials::Default.new
end

#get_git_username(url, username_from_url) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/r10k/git/rugged/credentials.rb', line 128

def get_git_username(url, username_from_url)
  git_user = R10K::Git.settings[:username]

  user = nil

  if !username_from_url.nil?
    user = username_from_url
    logger.debug2 _("URL %{url} includes the username %{username}, using that user for authentication.") % {url: url.inspect, username: username_from_url}
  elsif git_user
    user = git_user
    logger.debug2 _("URL %{url} did not specify a user, using %{user} from configuration") % {url: url.inspect, user: user.inspect}
  else
    user = Etc.getlogin
    logger.debug2 _("URL %{url} did not specify a user, using current user %{user}") % {url: url.inspect, user: user.inspect}
  end

  user
end

#get_plaintext_credentials(url, username_from_url) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/r10k/git/rugged/credentials.rb', line 67

def get_plaintext_credentials(url, username_from_url)
  per_repo_oauth_token = nil
  per_repo_github_app_id = nil
  per_repo_github_app_key = nil
  per_repo_github_app_ttl = nil

  if per_repo_settings = R10K::Git.get_repo_settings(url)
    per_repo_oauth_token = per_repo_settings[:oauth_token]
    per_repo_github_app_id = per_repo_settings[:github_app_id]
    per_repo_github_app_key = per_repo_settings[:github_app_key]
    per_repo_github_app_ttl = per_repo_settings[:github_app_ttl]
  end

  app_id = per_repo_github_app_id || R10K::Git.settings[:github_app_id]
  app_key = per_repo_github_app_key || R10K::Git.settings[:github_app_key]
  app_ttl = per_repo_github_app_ttl || R10K::Git.settings[:github_app_ttl]

  if token_path = per_repo_oauth_token || R10K::Git.settings[:oauth_token]
    @oauth_token ||= extract_token(token_path, url)

    user = 'x-oauth-token'
    password = @oauth_token
  elsif app_id && app_key && app_ttl
    user = 'x-access-token'
    password = github_app_token(app_id, app_key, app_ttl)
  else
    user = get_git_username(url, username_from_url)
    password = URI.parse(url).password || ''
  end
  Rugged::Credentials::UserPassword.new(username: user, password: password)
end

#get_ssh_key_credentials(url, username_from_url) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/r10k/git/rugged/credentials.rb', line 40

def get_ssh_key_credentials(url, username_from_url)
  user = get_git_username(url, username_from_url)

  per_repo_private_key = nil
  if per_repo_settings = R10K::Git.get_repo_settings(url)
    per_repo_private_key = per_repo_settings[:private_key]
  end

  global_private_key = R10K::Git.settings[:private_key]

  if per_repo_private_key
    private_key = per_repo_private_key
    logger.debug2 _("Using per-repository private key %{key} for URL %{url}") % {key: private_key, url: url.inspect}
  elsif global_private_key
    private_key = global_private_key
    logger.debug2 _("URL %{url} has no per-repository private key using '%{key}'." ) % {key: private_key, url: url.inspect}
  else
    raise R10K::Git::GitError.new(_("Git remote %{url} uses the SSH protocol but no private key was given") % {url: url.inspect}, :git_dir => @repository.path.to_s)
  end

  if !File.readable?(private_key)
    raise R10K::Git::GitError.new(_("Unable to use SSH key auth for %{url}: private key %{private_key} is missing or unreadable") % {url: url.inspect, private_key: private_key.inspect}, :git_dir => @repository.path.to_s)
  end

  Rugged::Credentials::SshKey.new(:username => user, :privatekey => private_key)
end

#github_app_token(app_id, private_key, ttl) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/r10k/git/rugged/credentials.rb', line 147

def github_app_token(app_id, private_key, ttl)
  raise R10K::Git::GitError, _('Github App id contains invalid characters.') unless app_id =~ /^\d+$/
  raise R10K::Git::GitError, _('Github App token ttl contains invalid characters.') unless ttl =~ /^\d+$/
  raise R10K::Git::GitError, _('Github App key is missing or unreadable') unless File.readable?(private_key)

  begin
    ssl_key = OpenSSL::PKey::RSA.new(File.read(private_key).strip)
    unless ssl_key.private?
      raise R10K::Git::GitError, _('Github App key is not a valid SSL private key')
    end
  rescue OpenSSL::PKey::RSAError
    raise R10K::Git::GitError, _('Github App key is not a valid SSL key')
  end

  logger.debug2 _("Using Github App id %{app_id} with SSL key from %{key_path}") % { key_path: private_key, app_id: app_id }

  jwt_issue_time = Time.now.to_i - 60
  jwt_exp_time = (jwt_issue_time + 60) + ttl.to_i
  payload = { iat: jwt_issue_time, exp: jwt_exp_time, iss: app_id }
  jwt = JWT.encode(payload, ssl_key, "RS256")

  get = URI.parse("https://api.github.com/app/installations")
  get_request = Net::HTTP::Get.new(get)
  get_request["Authorization"] = "Bearer #{jwt}"
  get_request["Accept"] = "application/vnd.github.v3+json"
  get_req_options = { use_ssl: get.scheme == "https", }
  get_response = Net::HTTP.start(get.hostname, get.port, get_req_options) do |http|
    http.request(get_request)
  end

  unless (get_response.class < Net::HTTPSuccess)
    logger.debug2 _("Unexpected response code: #{get_response.code}\nResponse body: #{get_response.body}")
    raise R10K::Git::GitError, _("Error using private key to get Github App access token from url")
  end

  access_tokens_url = JSON.parse(get_response.body)[0]['access_tokens_url']

  post = URI.parse(access_tokens_url)
  post_request = Net::HTTP::Post.new(post)
  post_request["Authorization"] = "Bearer #{jwt}"
  post_request["Accept"] = "application/vnd.github.v3+json"
  post_req_options = { use_ssl: post.scheme == "https", }
  post_response = Net::HTTP.start(post.hostname, post.port, post_req_options) do |http|
    http.request(post_request)
  end

  unless (post_response.class < Net::HTTPSuccess)
    logger.debug2 _("Unexpected response code: #{post_response.code}\nResponse body: #{post_response.body}")
    raise R10K::Git::GitError, _("Error using private key to generate access token from #{access_token_url}")
  end

  token = JSON.parse(post_response.body)['token']

  raise R10K::Git::GitError, _("Github App token contains invalid characters.") unless valid_token?(token)

  logger.debug2 _("Github App token generated, expires at: %{expire}") % {expire: JSON.parse(post_response.body)['expires_at']}
  token
end

#valid_token?(token) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This regex is the only real requirement for OAuth token format, per www.oauth.com/oauth2-servers/access-tokens/access-token-response/ Bitbucket’s tokens also can include an underscore, so that is added here.

Returns:

  • (Boolean)


120
121
122
# File 'lib/r10k/git/rugged/credentials.rb', line 120

def valid_token?(token)
  return token =~ /^[\w\-\.~_\+\/]+$/
end