class Vault::Client

Constants

DEFAULT_HEADERS

The default headers that are sent with every request.

JSON_PARSE_OPTIONS

The default list of options to use when parsing JSON.

LOCATION_HEADER

The name of the header used for redirection.

NAMESPACE_HEADER

The name of the header used to hold the Namespace.

RESCUED_EXCEPTIONS
TOKEN_HEADER

The name of the header used to hold the Vault token.

USER_AGENT

The user agent for this client.

WRAP_TTL_HEADER

The name of the header used to hold the wrapped request ttl.

Public Class Methods

new(options = {}) click to toggle source

Create a new Client with the given options. Any options given take precedence over the default options.

@return [Vault::Client]

# File lib/vault/client.rb, line 74
def initialize(options = {})
  # Use any options given, but fall back to the defaults set on the module
  Vault::Configurable.keys.each do |key|
    value = options.key?(key) ? options[key] : Defaults.public_send(key)
    instance_variable_set(:"@#{key}", value)
  end

  @lock = Mutex.new
  @nhp = nil
end

Public Instance Methods

approle() click to toggle source

A proxy to the {AppRole} methods. @return [AppRole]

# File lib/vault/api/approle.rb, line 12
def approle
  @approle ||= AppRole.new(self)
end
auth() click to toggle source

A proxy to the {Auth} methods. @return [Auth]

# File lib/vault/api/auth.rb, line 10
def auth
  @auth ||= Authenticate.new(self)
end
auth_tls() click to toggle source

A proxy to the {AuthTLS} methods. @return [AuthTLS]

# File lib/vault/api/auth_tls.rb, line 12
def auth_tls
  @auth_tls ||= AuthTLS.new(self)
end
auth_token() click to toggle source

A proxy to the {AuthToken} methods. @return [AuthToken]

# File lib/vault/api/auth_token.rb, line 12
def auth_token
  @auth_token ||= AuthToken.new(self)
end
build_uri(verb, path, params = {}) click to toggle source

Construct a URL from the given verb and path. If the request is a GET or DELETE request, the params are assumed to be query params are are converted as such using {Client#to_query_string}.

If the path is relative, it is merged with the {Defaults.address} attribute. If the path is absolute, it is converted to a URI object and returned.

@param [Symbol] verb

the lowercase HTTP verb (e.g. :+get+)

@param [String] path

the absolute or relative HTTP path (url) to get

@param [Hash] params

the list of params to build the URI with (for GET and DELETE requests)

@return [URI]

# File lib/vault/client.rb, line 323
def build_uri(verb, path, params = {})
  # Add any query string parameters
  if [:delete, :get].include?(verb)
    path = [path, to_query_string(params)].compact.join("?")
  end

  # Parse the URI
  uri = URI.parse(path)

  # Don't merge absolute URLs
  uri = URI.parse(File.join(address, path)) unless uri.absolute?

  # Return the URI object
  uri
end
class_for_request(verb) click to toggle source

Helper method to get the corresponding {Net::HTTP} class from the given HTTP verb.

@param [#to_s] verb

the HTTP verb to create a class from

@return [Class]

# File lib/vault/client.rb, line 346
def class_for_request(verb)
  Net::HTTP.const_get(verb.to_s.capitalize)
end
delete(path, params = {}, headers = {}) click to toggle source

Perform a DELETE request. @see Client#request

# File lib/vault/client.rb, line 217
def delete(path, params = {}, headers = {})
  request(:delete, path, params, headers)
end
error(response) click to toggle source

Raise a response error, extracting as much information from the server's response as possible.

@raise [HTTPError]

@param [HTTP::Message] response

the response object from the request
# File lib/vault/client.rb, line 388
def error(response)
  if response.body && response.body.match("missing client token")
    raise MissingTokenError
  end

  # Use the correct exception class
  case response
  when Net::HTTPClientError
    klass = HTTPClientError
  when Net::HTTPServerError
    klass = HTTPServerError
  else
    klass = HTTPError
  end

  if (response.content_type || '').include?("json")
    # Attempt to parse the error as JSON
    begin
      json = JSON.parse(response.body, JSON_PARSE_OPTIONS)

      if json[:errors]
        raise klass.new(address, response, json[:errors])
      end
    rescue JSON::ParserError; end
  end

  raise klass.new(address, response, [response.body])
end
get(path, params = {}, headers = {}) click to toggle source

Perform a GET request. @see Client#request

# File lib/vault/client.rb, line 186
def get(path, params = {}, headers = {})
  request(:get, path, params, headers)
end
help(path) click to toggle source

Gets help for the given path.

@example

Vault.help("secret") #=> #<Vault::Help help="..." see_also="...">

@param [String] path

the path to get help for

@return [Help]

# File lib/vault/api/help.rb, line 28
def help(path)
  json = self.get("/v1/#{EncodePath.encode_path(path)}", help: 1)
  return Help.decode(json)
end
kv(mount) click to toggle source

A proxy to the {KV} methods. @return [KV]

# File lib/vault/api/kv.rb, line 10
def kv(mount)
  KV.new(self, mount)
end
list(path, params = {}, headers = {}) click to toggle source

Perform a LIST request. @see Client#request

# File lib/vault/client.rb, line 192
def list(path, params = {}, headers = {})
  params = params.merge(list: true)
  request(:get, path, params, headers)
end
logical() click to toggle source

A proxy to the {Logical} methods. @return [Logical]

# File lib/vault/api/logical.rb, line 10
def logical
  @logical ||= Logical.new(self)
end
patch(path, data, headers = {}) click to toggle source

Perform a PATCH request. @see Client#request

# File lib/vault/client.rb, line 211
def patch(path, data, headers = {})
  request(:patch, path, data, headers)
end
post(path, data = {}, headers = {}) click to toggle source

Perform a POST request. @see Client#request

# File lib/vault/client.rb, line 199
def post(path, data = {}, headers = {})
  request(:post, path, data, headers)
end
put(path, data, headers = {}) click to toggle source

Perform a PUT request. @see Client#request

# File lib/vault/client.rb, line 205
def put(path, data, headers = {})
  request(:put, path, data, headers)
end
request(verb, path, data = {}, headers = {}) click to toggle source

Make an HTTP request with the given verb, data, params, and headers. If the response has a return type of JSON, the JSON is automatically parsed and returned as a hash; otherwise it is returned as a string.

@raise [HTTPError]

if the request is not an HTTP 200 OK

@param [Symbol] verb

the lowercase symbol of the HTTP verb (e.g. :get, :delete)

@param [String] path

the absolute or relative path from {Defaults.address} to make the
request against

@param [#read, Hash, nil] data

the data to use (varies based on the +verb+)

@param [Hash] headers

the list of headers to use

@return [String, Hash]

the response body
# File lib/vault/client.rb, line 240
def request(verb, path, data = {}, headers = {})
  # Build the URI and request object from the given information
  uri = build_uri(verb, path, data)
  request = class_for_request(verb).new(uri.request_uri)
  if uri.userinfo()
    request.basic_auth uri.user, uri.password
  end

  if proxy_address and uri.scheme.downcase == "https"
    raise SecurityError, "no direct https connection to vault"
  end

  # Get a list of headers
  headers = DEFAULT_HEADERS.merge(headers)

  # Add the Vault token header - users could still override this on a
  # per-request basis
  if !token.nil?
    headers[TOKEN_HEADER] ||= token
  end

  # Add the Vault Namespace header - users could still override this on a
  # per-request basis
  if !namespace.nil?
    headers[NAMESPACE_HEADER] ||= namespace
  end

  # Add headers
  headers.each do |key, value|
    request.add_field(key, value)
  end

  # Setup PATCH/POST/PUT
  if [:patch, :post, :put].include?(verb)
    if data.respond_to?(:read)
      request.content_length = data.size
      request.body_stream = data
    elsif data.is_a?(Hash)
      request.form_data = data
    else
      request.body = data
    end
  end

  begin
    # Create a connection using the block form, which will ensure the socket
    # is properly closed in the event of an error.
    response = pool.request(uri, request)

    case response
    when Net::HTTPRedirection
      # On a redirect of a GET or HEAD request, the URL already contains
      # the data as query string parameters.
      if [:head, :get].include?(verb)
        data = {}
      end
      request(verb, response[LOCATION_HEADER], data, headers)
    when Net::HTTPSuccess
      success(response)
    else
      error(response)
    end
  rescue *RESCUED_EXCEPTIONS => e
    raise HTTPConnectionError.new(address, e)
  end
end
same_options?(opts) click to toggle source

Determine if the given options are the same as ours. @return [true, false]

# File lib/vault/client.rb, line 180
def same_options?(opts)
  options.hash == opts.hash
end
shutdown() click to toggle source

Shutdown any open pool connections. Pool will be recreated upon next request.

# File lib/vault/client.rb, line 161
def shutdown
  @nhp.shutdown()
  @nhp = nil
end
success(response) click to toggle source

Parse the response object and manipulate the result based on the given Content-Type header. For now, this method only parses JSON, but it could be expanded in the future to accept other content types.

@param [HTTP::Message] response

the response object from the request

@return [String, Hash]

the parsed response, as an object
# File lib/vault/client.rb, line 373
def success(response)
  if response.body && (response.content_type || '').include?("json")
    JSON.parse(response.body, JSON_PARSE_OPTIONS)
  else
    response.body
  end
end
sys() click to toggle source

A proxy to the {Sys} methods. @return [Sys]

# File lib/vault/api/sys.rb, line 9
def sys
  @sys ||= Sys.new(self)
end
to_query_string(hash) click to toggle source

Convert the given hash to a list of query string parameters. Each key and value in the hash is URI-escaped for safety.

@param [Hash] hash

the hash to create the query string from

@return [String, nil]

the query string as a string, or +nil+ if there are no params
# File lib/vault/client.rb, line 358
def to_query_string(hash)
  hash.map do |key, value|
    "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
  end.join('&')[/.+/]
end
transform() click to toggle source

A proxy to the {Transform} methods. @return [Transform]

# File lib/vault/api/transform.rb, line 8
def transform
  @transform ||= Transform.new(self)
end
with_retries(*rescued) { |retries, exception| ... } click to toggle source

Execute the given block with retries and exponential backoff.

@param [Array<Exception>] rescued

the list of exceptions to rescue
# File lib/vault/client.rb, line 421
def with_retries(*rescued, &block)
  options      = rescued.last.is_a?(Hash) ? rescued.pop : {}
  exception    = nil
  retries      = 0

  rescued = Defaults::RETRIED_EXCEPTIONS if rescued.empty?

  max_attempts = options[:attempts] || Defaults::RETRY_ATTEMPTS
  backoff_base = options[:base]     || Defaults::RETRY_BASE
  backoff_max  = options[:max_wait] || Defaults::RETRY_MAX_WAIT

  begin
    return yield retries, exception
  rescue *rescued => e
    exception = e

    retries += 1
    raise if retries > max_attempts

    # Calculate the exponential backoff combined with an element of
    # randomness.
    backoff = [backoff_base * (2 ** (retries - 1)), backoff_max].min
    backoff = backoff * (0.5 * (1 + Kernel.rand))

    # Ensure we are sleeping at least the minimum interval.
    backoff = [backoff_base, backoff].max

    # Exponential backoff.
    Kernel.sleep(backoff)

    # Now retry
    retry
  end
end
with_token(token) { |client| ... } click to toggle source

Creates and yields a new client object with the given token. This may be used safely in a threadsafe manner because the original client remains unchanged. The value of the block is returned.

@yield [Vault::Client]

# File lib/vault/client.rb, line 171
def with_token(token)
  client = self.dup
  client.token = token
  return yield client if block_given?
  return nil
end

Private Instance Methods

pool() click to toggle source
# File lib/vault/client.rb, line 85
def pool
  @lock.synchronize do
    return @nhp if @nhp

    @nhp = PersistentHTTP.new("vault-ruby", nil, pool_size)

    if proxy_address
      proxy_uri = URI.parse "http://#{proxy_address}"

      proxy_uri.port = proxy_port if proxy_port

      if proxy_username
        proxy_uri.user = proxy_username
        proxy_uri.password = proxy_password
      end

      @nhp.proxy = proxy_uri
    end

    # Use a custom open timeout
    if open_timeout || timeout
      @nhp.open_timeout = (open_timeout || timeout).to_i
    end

    # Use a custom read timeout
    if read_timeout || timeout
      @nhp.read_timeout = (read_timeout || timeout).to_i
    end

    @nhp.verify_mode = OpenSSL::SSL::VERIFY_PEER

    # Vault requires TLS1.2
    @nhp.ssl_version = "TLSv1_2"

    # Only use secure ciphers
    @nhp.ciphers = ssl_ciphers

    # Custom pem files, no problem!
    pem = ssl_pem_contents || (ssl_pem_file ? File.read(ssl_pem_file) : nil)
    if pem
      @nhp.cert = OpenSSL::X509::Certificate.new(pem)
      @nhp.key = OpenSSL::PKey::RSA.new(pem, ssl_pem_passphrase)
    end

    # Use custom CA cert for verification
    if ssl_ca_cert
      @nhp.ca_file = ssl_ca_cert
    end

    # Use custom CA path that contains CA certs
    if ssl_ca_path
      @nhp.ca_path = ssl_ca_path
    end

    if ssl_cert_store
      @nhp.cert_store = ssl_cert_store
    end

    # Naughty, naughty, naughty! Don't blame me when someone hops in
    # and executes a MITM attack!
    if !ssl_verify
      @nhp.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end

    # Use custom timeout for connecting and verifying via SSL
    if ssl_timeout || timeout
      @nhp.ssl_timeout = (ssl_timeout || timeout).to_i
    end

    @nhp
  end
end