Class: DuoApi

Inherits:
Object
  • Object
show all
Defined in:
lib/duo_api/api_client.rb,
lib/duo_api/auth.rb,
lib/duo_api/admin.rb,
lib/duo_api/device.rb,
lib/duo_api/accounts.rb,
lib/duo_api/api_helpers.rb

Overview

Extend DuoApi class with some HTTP method helpers

Direct Known Subclasses

Accounts, Admin, Auth, Device

Defined Under Namespace

Classes: Accounts, Admin, Auth, ChildAccountError, ContentTypeError, Device, HeaderError, PaginationError, RateLimitError, ResponseCodeError

Constant Summary collapse

VERSION =
Gem.loaded_specs['duo_api'] ? Gem.loaded_specs['duo_api'].version : '0.0.0'
MAX_BACKOFF_WAIT_SECS =

Constants for handling rate limit backoff

32
INITIAL_BACKOFF_WAIT_SECS =
1
BACKOFF_FACTOR =
2

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ikey, skey, host, proxy = nil, ca_file: nil, default_params: {}) ⇒ DuoApi

Returns a new instance of DuoApi.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/duo_api/api_client.rb', line 25

def initialize(ikey, skey, host, proxy = nil, ca_file: nil, default_params: {})
  @ikey = ikey
  @skey = skey
  @host = host
  @proxy_str = proxy
  if proxy.nil?
    @proxy = []
  else
    proxy_uri = URI.parse proxy
    @proxy = [
      proxy_uri.host,
      proxy_uri.port,
      proxy_uri.user,
      proxy_uri.password
    ]
  end
  @ca_file = ca_file ||
             File.join(File.dirname(__FILE__), '..', '..', 'ca_certs.pem')
  @default_params = default_params.transform_keys(&:to_sym)
end

Instance Attribute Details

#ca_fileObject

Returns the value of attribute ca_file.



15
16
17
# File 'lib/duo_api/api_client.rb', line 15

def ca_file
  @ca_file
end

#default_paramsObject

Returns the value of attribute default_params.



16
17
18
# File 'lib/duo_api/api_client.rb', line 16

def default_params
  @default_params
end

Instance Method Details

#delete(path, params = {}, additional_headers = nil) ⇒ Object

Perform a DELETE request and parse the response as JSON



127
128
129
130
131
132
133
# File 'lib/duo_api/api_helpers.rb', line 127

def delete(path, params = {}, additional_headers = nil)
  resp = request('DELETE', path, params, additional_headers)
  raise_http_errors(resp)
  raise_content_type_errors(resp[:'content-type'], 'application/json')

  parse_json_to_sym_hash(resp.body)
end

#get(path, params = {}, additional_headers = nil) ⇒ Object

Perform a GET request and parse the response as JSON



8
9
10
11
12
13
14
# File 'lib/duo_api/api_helpers.rb', line 8

def get(path, params = {}, additional_headers = nil)
  resp = request('GET', path, params, additional_headers)
  raise_http_errors(resp)
  raise_content_type_errors(resp[:'content-type'], 'application/json')

  parse_json_to_sym_hash(resp.body)
end

#get_all(path, params = {}, additional_headers = nil, data_array_path: nil, metadata_path: nil) ⇒ Object

Perform a GET request and retrieve all paginated JSON data



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
66
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/duo_api/api_helpers.rb', line 17

def get_all(path, params = {}, additional_headers = nil, data_array_path: nil, metadata_path: nil)
  # Set default paths for returned data array and metadata if not provided
  data_array_path = if data_array_path.is_a?(Array) && (data_array_path.count >= 1)
                      data_array_path.map(&:to_sym)
                    else
                      [:response]
                    end
   = if .is_a?(Array) && (.count >= 1)
                    .map(&:to_sym)
                  else
                    [:metadata]
                  end

  # Ensure params keys are symbols and ignore offset parameters
  params.transform_keys!(&:to_sym)
  %i[offset next_offset].each do |p|
    if params[p]
      warn "Ignoring supplied #{p} parameter for get_all method"
      params.delete(p)
    end
  end
  # Default :limit to 1000 unless specified to minimize requests
  params[:limit] ||= 1000

  all_data = []
  prev_results_count = 0
  next_offset = 0
  prev_offset = 0
  resp_body_hash = {}
  loop do
    resp = request('GET', path, params, additional_headers)
    raise_http_errors(resp)
    raise_content_type_errors(resp[:'content-type'], 'application/json')

    resp_body_hash = parse_json_to_sym_hash(resp.body)
    resp_data_array = resp_body_hash.dig(*data_array_path)
    unless resp_data_array.is_a?(Array)
      raise(PaginationError,
            "Object at data_array_path #{JSON.generate(data_array_path)} is not an Array")
    end
    all_data.concat(resp_data_array)

     = resp_body_hash.dig(*)
    if .is_a?(Hash) && [:next_offset]
      next_offset = [:next_offset]
      next_offset = next_offset.to_i if string_int?(next_offset)

      if next_offset.is_a?(Array) || next_offset.is_a?(String)
        next_offset = next_offset.join(',') if next_offset.is_a?(Array)
        raise(PaginationError, 'Paginated response offset error') if next_offset == prev_offset

        params[:next_offset] = next_offset
      else
        raise(PaginationError, 'Paginated response offset error') if next_offset <= prev_offset

        params[:offset] = next_offset
      end
    else
      next_offset = nil
      params.delete(:offset)
      params.delete(:next_offset)
    end

    break if !next_offset ||
             (all_data.count <= prev_results_count)

    prev_results_count = all_data.count
    prev_offset = next_offset
  end

  # Replace the data array in the last returned resp_body_hash with the all_data array
  data_array_parent_hash = if data_array_path.count > 1
                             resp_body_hash.dig(*data_array_path[0..-2])
                           else
                             resp_body_hash
                           end
  data_array_key = data_array_path.last
  data_array_parent_hash[data_array_key] = all_data

  resp_body_hash
end

#get_image(path, params = {}, additional_headers = nil) ⇒ Object

Perform a GET request to retrieve image data and return raw data



100
101
102
103
104
105
106
# File 'lib/duo_api/api_helpers.rb', line 100

def get_image(path, params = {}, additional_headers = nil)
  resp = request('GET', path, params, additional_headers)
  raise_http_errors(resp)
  raise_content_type_errors(resp[:'content-type'], %r{^image/})

  resp.body
end

#post(path, params = {}, additional_headers = nil) ⇒ Object

Perform a POST request and parse the response as JSON



109
110
111
112
113
114
115
# File 'lib/duo_api/api_helpers.rb', line 109

def post(path, params = {}, additional_headers = nil)
  resp = request('POST', path, params, additional_headers)
  raise_http_errors(resp)
  raise_content_type_errors(resp[:'content-type'], 'application/json')

  parse_json_to_sym_hash(resp.body)
end

#put(path, params = {}, additional_headers = nil) ⇒ Object

Perform a PUT request and parse the response as JSON



118
119
120
121
122
123
124
# File 'lib/duo_api/api_helpers.rb', line 118

def put(path, params = {}, additional_headers = nil)
  resp = request('PUT', path, params, additional_headers)
  raise_http_errors(resp)
  raise_content_type_errors(resp[:'content-type'], 'application/json')

  parse_json_to_sym_hash(resp.body)
end

#request(method, path, params = {}, additional_headers = nil) ⇒ Object

Basic authenticated request returning raw Net::HTTPResponse object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
98
99
100
# File 'lib/duo_api/api_client.rb', line 51

def request(method, path, params = {}, additional_headers = nil)
  # Merge default params with provided params
  params = @default_params.merge(params.transform_keys(&:to_sym))

  # Determine if params should be in a JSON request body
  params_go_in_body = %w[POST PUT PATCH].include?(method)
  if params_go_in_body
    body = canon_json(params)
    params = {}
  else
    body = ''
  end

  # Construct the request URI
  uri = request_uri(path, params)

  # Sign the request
  current_date, signed = sign(method, uri.host, path, params, body, additional_headers)

  # Create the HTTP request object
  request = Net::HTTP.const_get(method.capitalize).new(uri.to_s)
  request.basic_auth(@ikey, signed)
  request['Date'] = current_date
  request['User-Agent'] = "duo_api_ruby/#{VERSION}"

  # Set Content-Type and request body for JSON requests
  if params_go_in_body
    request['Content-Type'] = 'application/json'
    request.body = body
  end

  # Start the HTTP session
  Net::HTTP.start(
    uri.host, uri.port, *@proxy,
    use_ssl: true, ca_file: @ca_file,
    verify_mode: OpenSSL::SSL::VERIFY_PEER
  ) do |http|
    wait_secs = INITIAL_BACKOFF_WAIT_SECS
    loop do
      resp = http.request(request)

      # Check if the response is rate-limited and handle backoff
      return resp if !resp.is_a?(Net::HTTPTooManyRequests) || (wait_secs > MAX_BACKOFF_WAIT_SECS)

      random_offset = rand
      sleep(wait_secs + random_offset)
      wait_secs *= BACKOFF_FACTOR
    end
  end
end