Class: Aptible::CLI::Agent

Constant Summary

Constants included from Subcommands::MetricDrain

Subcommands::MetricDrain::PATH, Subcommands::MetricDrain::SITES

Constants included from Subcommands::Deploy

Subcommands::Deploy::DOCKER_IMAGE_DEPLOY_ARGS, Subcommands::Deploy::NULL_SHA1

Constants included from Helpers::Token

Helpers::Token::TOKEN_ENV_VAR

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Subcommands::BackupRetentionPolicy

included

Methods included from Subcommands::Maintenance

#explanation, included, #no_maintenances

Methods included from Subcommands::MetricDrain

included

Methods included from Subcommands::LogDrain

included

Methods included from Subcommands::Endpoints

included

Methods included from Subcommands::Inspect

included, #inspect_resource

Methods included from Subcommands::Operation

included

Methods included from Subcommands::Backup

included

Methods included from Subcommands::SSH

included

Methods included from Subcommands::Services

included

Methods included from Subcommands::Restart

included

Methods included from Subcommands::Deploy

included

Methods included from Subcommands::Rebuild

included

Methods included from Subcommands::Logs

included

Methods included from Subcommands::Environment

included

Methods included from Subcommands::DB

included

Methods included from Subcommands::Config

included

Methods included from Subcommands::Apps

included

Methods included from Helpers::DateHelpers

#utc_date, #utc_datetime, #utc_string

Methods included from Helpers::ConfigPath

#aptible_config_path

Methods included from Helpers::System

#ask_then_line, #which

Methods included from Helpers::Ssh

#connect_to_ssh_portal, #exit_with_ssh_portal, #with_ssh_cmd

Methods included from Helpers::Token

#current_token_hash, #fetch_token, #save_token, #token_file

Constructor Details

#initializeAgent

Returns a new instance of Agent.



81
82
83
84
85
86
# File 'lib/aptible/cli/agent.rb', line 81

def initialize(*)
  nag_toolbelt unless toolbelt?
  Aptible::Resource.configure { |conf| conf.user_agent = version_string }
  warn_sso_enforcement
  super
end

Class Method Details

.exit_on_failure?Boolean

Forward return codes on failures.

Returns:

  • (Boolean)


77
78
79
# File 'lib/aptible/cli/agent.rb', line 77

def self.exit_on_failure?
  true
end

Instance Method Details

#loginObject



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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
205
206
207
208
209
210
211
212
# File 'lib/aptible/cli/agent.rb', line 105

def 
  if options[:sso]
    begin
      token = options[:sso]
      token = ask('Paste token copied from Dashboard:') if token == 'sso'
      Base64.urlsafe_decode64(token.split('.').first)
      save_token(token)
      CLI.logger.info "Token written to #{token_file}"
      return
    rescue StandardError
      raise Thor::Error, 'Invalid token provided for SSO'
    end
  end

  email = options[:email] || ask('Email: ')
  password = options[:password] || ask_then_line(
    'Password: ', echo: false
  )

  token_options = { email: email, password: password }

  otp_token = options[:otp_token]
  token_options[:otp_token] = otp_token if otp_token

  begin
    lifetime = '1w'
    lifetime = '12h' if token_options[:otp_token] || token_options[:u2f]
    lifetime = options[:lifetime] if options[:lifetime]

    duration = ChronicDuration.parse(lifetime)
    if duration.nil?
      raise Thor::Error, "Invalid token lifetime requested: #{lifetime}"
    end

    token_options[:expires_in] = duration
    token = Aptible::Auth::Token.create(token_options)
  rescue OAuth2::Error => e
    # If a MFA is require but a token wasn't provided,
    # prompt the user for MFA authentication and retry
    if e.code != 'otp_token_required'
      raise Thor::Error, 'Could not authenticate with given ' \
                         "credentials: #{e.code}"
    end

    u2f = (e.response.parsed['exception_context'] || {})['u2f']

    q = Queue.new
    mfa_threads = []

    # If the user has added a security key and their computer supports it,
    # allow them to use it
    # https://developers.yubico.com/libfido2/Manuals
    # installation: https://github.com/Yubico/libfido2#installation
    if u2f && !which('fido2-assert').nil? && !which('fido2-token').nil?
      origin = Aptible::Auth::Resource.new.get.href
      app_id = Aptible::Auth::Resource.new.utf_trusted_facets.href
      challenge = u2f.fetch('challenge')

      device_info = security_key_device(u2f, app_id)

      if device_info[:locations].count > 0 && device_info[:device]
        puts "\nEnter your 2FA token or touch your Security Key " \
           'once it starts blinking.'

        mfa_threads << Thread.new do
          token_options[:u2f] = Helpers::SecurityKey.authenticate(
            origin,
            app_id,
            challenge,
            device_info[:device],
            device_info[:locations]
          )

          puts ''

          q.push(nil)
        end
      end
    end

    mfa_threads << Thread.new do
      token_options[:otp_token] = options[:otp_token] || ask(
        '2FA Token: '
      )

      q.push(nil)
    end

    # Block until one of the threads completes
    q.pop

    mfa_threads.each do |thr|
      sleep 0.5 until thr.status != 'run'
      thr.kill
    end.each(&:join)

    retry
  end

  save_token(token.access_token)
  CLI.logger.info "Token written to #{token_file}"

  lifetime_format = { units: 2, joiner: ', ' }
  token_lifetime = (token.expires_at - token.created_at).round
  expires_in = ChronicDuration.output(token_lifetime, lifetime_format)
  CLI.logger.info "This token will expire after #{expires_in} " \
                  '(use --lifetime to customize)'
end

#versionObject



89
90
91
92
93
94
95
# File 'lib/aptible/cli/agent.rb', line 89

def version
  Formatter.render(Renderer.current) do |root|
    root.keyed_object('version') do |node|
      node.value('version', version_string)
    end
  end
end