onelogin 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/README.md +75 -5
  4. data/examples/Gemfile.lock +15 -11
  5. data/examples/events-to-csv.rb +5 -4
  6. data/lib/onelogin/api/apiexception.rb +3 -0
  7. data/lib/onelogin/api/client.rb +109 -18
  8. data/lib/onelogin/api/cursor.rb +46 -3
  9. data/lib/onelogin/api/models/app.rb +8 -1
  10. data/lib/onelogin/api/models/user.rb +2 -1
  11. data/lib/onelogin/api/util/constants.rb +18 -0
  12. data/lib/onelogin/api/util/parser.rb +6 -2
  13. data/lib/onelogin/api/util/url_builder.rb +25 -1
  14. data/lib/onelogin/version.rb +1 -1
  15. data/onelogin.gemspec +2 -2
  16. metadata +6 -115
  17. data/.travis.yml +0 -6
  18. data/examples/rails-custom-login-page/.gitignore +0 -45
  19. data/examples/rails-custom-login-page/.ruby-version +0 -1
  20. data/examples/rails-custom-login-page/Gemfile +0 -57
  21. data/examples/rails-custom-login-page/Gemfile.lock +0 -212
  22. data/examples/rails-custom-login-page/README.md +0 -137
  23. data/examples/rails-custom-login-page/Rakefile +0 -6
  24. data/examples/rails-custom-login-page/app/assets/config/manifest.js +0 -3
  25. data/examples/rails-custom-login-page/app/assets/images/.keep +0 -0
  26. data/examples/rails-custom-login-page/app/assets/javascripts/application.js +0 -18
  27. data/examples/rails-custom-login-page/app/assets/javascripts/cable.js +0 -13
  28. data/examples/rails-custom-login-page/app/assets/javascripts/channels/.keep +0 -0
  29. data/examples/rails-custom-login-page/app/assets/javascripts/dashboard.coffee +0 -3
  30. data/examples/rails-custom-login-page/app/assets/javascripts/home.coffee +0 -3
  31. data/examples/rails-custom-login-page/app/assets/javascripts/sessions.coffee +0 -3
  32. data/examples/rails-custom-login-page/app/assets/stylesheets/application.css +0 -15
  33. data/examples/rails-custom-login-page/app/assets/stylesheets/dashboard.scss +0 -3
  34. data/examples/rails-custom-login-page/app/assets/stylesheets/home.scss +0 -3
  35. data/examples/rails-custom-login-page/app/assets/stylesheets/sessions.scss +0 -3
  36. data/examples/rails-custom-login-page/app/channels/application_cable/channel.rb +0 -4
  37. data/examples/rails-custom-login-page/app/channels/application_cable/connection.rb +0 -4
  38. data/examples/rails-custom-login-page/app/controllers/application_controller.rb +0 -23
  39. data/examples/rails-custom-login-page/app/controllers/concerns/.keep +0 -0
  40. data/examples/rails-custom-login-page/app/controllers/dashboard_controller.rb +0 -10
  41. data/examples/rails-custom-login-page/app/controllers/home_controller.rb +0 -5
  42. data/examples/rails-custom-login-page/app/controllers/sessions_controller.rb +0 -46
  43. data/examples/rails-custom-login-page/app/controllers/users_controller.rb +0 -120
  44. data/examples/rails-custom-login-page/app/helpers/application_helper.rb +0 -2
  45. data/examples/rails-custom-login-page/app/helpers/dashboard_helper.rb +0 -28
  46. data/examples/rails-custom-login-page/app/helpers/home_helper.rb +0 -2
  47. data/examples/rails-custom-login-page/app/helpers/sessions_helper.rb +0 -87
  48. data/examples/rails-custom-login-page/app/helpers/users_helper.rb +0 -3
  49. data/examples/rails-custom-login-page/app/jobs/application_job.rb +0 -2
  50. data/examples/rails-custom-login-page/app/mailers/application_mailer.rb +0 -4
  51. data/examples/rails-custom-login-page/app/models/application_record.rb +0 -3
  52. data/examples/rails-custom-login-page/app/models/concerns/.keep +0 -0
  53. data/examples/rails-custom-login-page/app/views/dashboard/index.html.erb +0 -43
  54. data/examples/rails-custom-login-page/app/views/home/index.html.erb +0 -272
  55. data/examples/rails-custom-login-page/app/views/layouts/application.html.erb +0 -31
  56. data/examples/rails-custom-login-page/app/views/layouts/mailer.html.erb +0 -13
  57. data/examples/rails-custom-login-page/app/views/layouts/mailer.text.erb +0 -1
  58. data/examples/rails-custom-login-page/app/views/users/_form.html.erb +0 -1
  59. data/examples/rails-custom-login-page/app/views/users/_user.json.jbuilder +0 -2
  60. data/examples/rails-custom-login-page/app/views/users/edit.html.erb +0 -33
  61. data/examples/rails-custom-login-page/app/views/users/index.html.erb +0 -33
  62. data/examples/rails-custom-login-page/app/views/users/index.json.jbuilder +0 -1
  63. data/examples/rails-custom-login-page/app/views/users/new.html.erb +0 -60
  64. data/examples/rails-custom-login-page/app/views/users/onboard.html.erb +0 -54
  65. data/examples/rails-custom-login-page/app/views/users/show.html.erb +0 -17
  66. data/examples/rails-custom-login-page/app/views/users/show.json.jbuilder +0 -1
  67. data/examples/rails-custom-login-page/bin/bundle +0 -3
  68. data/examples/rails-custom-login-page/bin/rails +0 -9
  69. data/examples/rails-custom-login-page/bin/rake +0 -9
  70. data/examples/rails-custom-login-page/bin/setup +0 -38
  71. data/examples/rails-custom-login-page/bin/spring +0 -17
  72. data/examples/rails-custom-login-page/bin/update +0 -29
  73. data/examples/rails-custom-login-page/bin/yarn +0 -11
  74. data/examples/rails-custom-login-page/config/application.rb +0 -18
  75. data/examples/rails-custom-login-page/config/boot.rb +0 -3
  76. data/examples/rails-custom-login-page/config/cable.yml +0 -10
  77. data/examples/rails-custom-login-page/config/database.yml +0 -25
  78. data/examples/rails-custom-login-page/config/environment.rb +0 -5
  79. data/examples/rails-custom-login-page/config/environments/development.rb +0 -54
  80. data/examples/rails-custom-login-page/config/environments/production.rb +0 -91
  81. data/examples/rails-custom-login-page/config/environments/test.rb +0 -42
  82. data/examples/rails-custom-login-page/config/initializers/application_controller_renderer.rb +0 -8
  83. data/examples/rails-custom-login-page/config/initializers/assets.rb +0 -14
  84. data/examples/rails-custom-login-page/config/initializers/backtrace_silencers.rb +0 -7
  85. data/examples/rails-custom-login-page/config/initializers/cookies_serializer.rb +0 -5
  86. data/examples/rails-custom-login-page/config/initializers/filter_parameter_logging.rb +0 -4
  87. data/examples/rails-custom-login-page/config/initializers/inflections.rb +0 -16
  88. data/examples/rails-custom-login-page/config/initializers/mime_types.rb +0 -4
  89. data/examples/rails-custom-login-page/config/initializers/onelogin.rb +0 -6
  90. data/examples/rails-custom-login-page/config/initializers/wrap_parameters.rb +0 -14
  91. data/examples/rails-custom-login-page/config/locales/en.yml +0 -33
  92. data/examples/rails-custom-login-page/config/puma.rb +0 -56
  93. data/examples/rails-custom-login-page/config/routes.rb +0 -23
  94. data/examples/rails-custom-login-page/config/secrets.yml.sample +0 -38
  95. data/examples/rails-custom-login-page/config/spring.rb +0 -6
  96. data/examples/rails-custom-login-page/config.ru +0 -5
  97. data/examples/rails-custom-login-page/db/seeds.rb +0 -7
  98. data/examples/rails-custom-login-page/lib/assets/.keep +0 -0
  99. data/examples/rails-custom-login-page/lib/tasks/.keep +0 -0
  100. data/examples/rails-custom-login-page/package.json +0 -5
  101. data/examples/rails-custom-login-page/public/404.html +0 -67
  102. data/examples/rails-custom-login-page/public/422.html +0 -67
  103. data/examples/rails-custom-login-page/public/500.html +0 -66
  104. data/examples/rails-custom-login-page/public/apple-touch-icon-precomposed.png +0 -0
  105. data/examples/rails-custom-login-page/public/apple-touch-icon.png +0 -0
  106. data/examples/rails-custom-login-page/public/favicon.ico +0 -0
  107. data/examples/rails-custom-login-page/public/robots.txt +0 -1
  108. data/examples/rails-custom-login-page/test/application_system_test_case.rb +0 -5
  109. data/examples/rails-custom-login-page/test/controllers/.keep +0 -0
  110. data/examples/rails-custom-login-page/test/controllers/dashboard_controller_test.rb +0 -9
  111. data/examples/rails-custom-login-page/test/controllers/home_controller_test.rb +0 -9
  112. data/examples/rails-custom-login-page/test/controllers/sessions_controller_test.rb +0 -7
  113. data/examples/rails-custom-login-page/test/controllers/users_controller_test.rb +0 -48
  114. data/examples/rails-custom-login-page/test/fixtures/.keep +0 -0
  115. data/examples/rails-custom-login-page/test/fixtures/files/.keep +0 -0
  116. data/examples/rails-custom-login-page/test/helpers/.keep +0 -0
  117. data/examples/rails-custom-login-page/test/integration/.keep +0 -0
  118. data/examples/rails-custom-login-page/test/mailers/.keep +0 -0
  119. data/examples/rails-custom-login-page/test/models/.keep +0 -0
  120. data/examples/rails-custom-login-page/test/system/.keep +0 -0
  121. data/examples/rails-custom-login-page/test/test_helper.rb +0 -9
  122. data/examples/rails-custom-login-page/vendor/.keep +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db38cae3acb3c8a5810159344a5e088b9e2b52467831e865b625462b470d6340
4
- data.tar.gz: b40ec7f43940358e3e2014a1075074778afd6d1d4263b3fa1ea2a6b78853e55e
3
+ metadata.gz: '0887ddcbf97b6c873d5547842d27822f35b7e4eeefa4c78285ed2828c0cf546b'
4
+ data.tar.gz: 5a59b6e4c2ff4605775a8473931ed82183d9f434d455a559214828cd612c1988
5
5
  SHA512:
6
- metadata.gz: d0e8f95dc8d30b514d1a0427b64364a2389a548057d643bda94a2a58cf6c44dc3bf33240b7d1d660ad5ff8c09595b4e4e1e7f7ad6382a5aa54fd044209219cda
7
- data.tar.gz: bafb58f70a588457fc592dd397c54069a87245df8d332d56369c36bb6842d3a1fb4e5cfa9250e0b346dc335f288f99a0e6a4fdc3f9fa0960413349155c8647c7
6
+ metadata.gz: 01b0e196b7f431a2d6e04a1b64757a4b0224b0a5b34e3b17ee6c829f642e6b321305d6ebe15dee6dae44fd15b92f64555927522994c469f7f8ba2a1964ccfb10
7
+ data.tar.gz: 2595a8b6f36f9a446e991baa18b66e37c9f2ac21687a43d7ca6f0c1dfa1b5c515a7edf8c9d57f3e64874d7690d79d807af31c3f689842fc21fda936bd8fd2b52
@@ -9,7 +9,7 @@ jobs:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- ruby: ['3.0', '3.1', '3.2', '3.3']
12
+ ruby: ['3.2', '3.3', '3.4']
13
13
 
14
14
  steps:
15
15
  - name: Checkout
data/README.md CHANGED
@@ -3,9 +3,15 @@
3
3
  This SDK will let you execute all the API methods, version/1, described
4
4
  at https://developers.onelogin.com/api-docs/1/getting-started/dev-overview.
5
5
 
6
- The toolkit is hosted on github. You can download it from:
6
+ The toolkit is hosted on GitHub. You can download it from:
7
7
  * Lastest release: https://github.com/onelogin/onelogin-ruby-sdk/releases/latest
8
- * Master repo: https://github.com/onelogin/onelogin-ruby-sdk/tree/master
8
+ * Main repo: https://github.com/onelogin/onelogin-ruby-sdk/tree/main
9
+
10
+
11
+ ## Support
12
+
13
+ OneLogin by One Identity open source projects are supported through [OneLogin GitHub issues](https://github.com/onelogin/onelogin-ruby-sdk/issues). This includes all scripts, plugins, SDKs, modules, code snippets or other solutions. For assistance with any OneLogin by One Identity GitHub project, please raise a new Issue on the [OneLogin GitHub issues](https://github.com/onelogin/onelogin-ruby-sdk/issues) page. Requests for assistance made through official One Identity Support will be referred back to GitHub where those requests can benefit all users.
14
+
9
15
 
10
16
 
11
17
  ## Installation
@@ -27,7 +33,17 @@ Or install it yourself as:
27
33
 
28
34
  ### Dependencies
29
35
 
36
+ * Ruby >= 3.2
30
37
  * httparty
38
+ * nokogiri
39
+
40
+ Ruby 3.2 is the lowest version on which `nokogiri` resolves to a release with
41
+ no known security advisories — below it, the newest installable `nokogiri` is
42
+ still affected by a critical one.
43
+
44
+ On an older Ruby, `bundle install` resolves to the most recent release that
45
+ still supports it rather than failing, so existing builds keep working. You
46
+ just stop receiving updates until you upgrade Ruby.
31
47
 
32
48
  ## Getting started
33
49
 
@@ -43,6 +59,7 @@ If you don't have an account you can [sign up for a free developer account here]
43
59
  |max_results| Optional: Defaults to 1000 |
44
60
  |timeout| Optional: Defaults to 60 (requires httparty > 0.16.2) |
45
61
  |token_expiration_buffer| Optional: Non-negative number of seconds before token expiration to trigger a refresh. Set to `0` to disable. Defaults to 30 |
62
+ |api_version| Optional: `1` or `2`. Calls that API version from every method that supports it. See [API versions](#api-versions) |
46
63
 
47
64
  ```ruby
48
65
  require 'onelogin'
@@ -142,6 +159,34 @@ client = OneLogin::Api::Client.new(
142
159
  client.get_events.map {|event| event.id}
143
160
  ```
144
161
 
162
+ ### API versions
163
+
164
+ Each method calls one version of the OneLogin API by default, and that default
165
+ doesn't change. Pass `api_version: 2` to move the methods below to API v2:
166
+
167
+ ```ruby
168
+ client = OneLogin::Api::Client.new(
169
+ client_id: '',
170
+ client_secret:'',
171
+ api_version: 2
172
+ )
173
+ ```
174
+
175
+ `get_users`, `get_user`, `get_user_apps`, `create_user`, `update_user`,
176
+ `delete_user`, `get_roles` and `get_role` support both versions. Every other method
177
+ keeps its default version, whatever `api_version` says.
178
+ `OneLogin::Api::Client::API_VERSIONS` lists the versions each method supports.
179
+
180
+ Under v2 they return the same models, filled from v2's responses. Two differences
181
+ show through:
182
+
183
+ - v2 lists fewer fields per user than v1. Under v2, `get_users` leaves `role_ids`
184
+ and `custom_attributes` empty, for example. `get_user` fills them.
185
+ - `App#login_id` from `get_user_apps` is the login's numeric id in v2, where v1
186
+ gives the username used to sign in. v2 fills the `provisioning_enabled`,
187
+ `provisioning_state` and `provisioning_status` fields rather than `provisioned`
188
+ and `personal`.
189
+
145
190
 
146
191
 
147
192
  ### Available Methods
@@ -167,17 +212,42 @@ query_parameters = {
167
212
  }
168
213
  users_filtered2 = client.get_users(query_parameters)
169
214
 
170
- # Get Users with limit
215
+ # Get Users with a page size of 3.
216
+ #
217
+ # `limit` is the API's page size, not a cap on the total. To stop after a
218
+ # fixed number of results use `take`, or set `max_results` on the client.
171
219
  query_parameters = {
172
220
  limit: 3
173
221
  }
174
- users_filtered_limited = client.get_users(query_parameters)
222
+ users_paged_by_three = client.get_users(query_parameters)
223
+
224
+ # Stop after 3 users regardless of page size
225
+ first_three = client.get_users.take(3)
175
226
 
176
227
  # Only return the firstname and email fields for each user
177
228
  client.get_users(fields: 'email,firstname').each do |user|
178
229
  puts "#{user.firstname} - #{user.email}"
179
230
  end
180
231
 
232
+ # Sort server-side with the sort parameter. Prefix with - to reverse.
233
+ # Sorting happens in the API, so you do not need to fetch every user and
234
+ # sort client-side.
235
+ query_parameters = {
236
+ sort: '+id'
237
+ }
238
+ users_sorted = client.get_users(query_parameters)
239
+
240
+ # Ten least recently active users.
241
+ #
242
+ # `sort` is applied by the API. `limit` sets the page size so the request
243
+ # itself only fetches ten rows, and `take` stops the cursor after ten so it
244
+ # does not page any further.
245
+ #
246
+ # Use both: `limit` alone does not cap the total, because the cursor keeps
247
+ # following pages until they run out or `max_results` is reached, and `take`
248
+ # alone still transfers a full default-sized page and discards the extra.
249
+ least_recently_active = client.get_users(sort: '+last_login', limit: 10).take(10)
250
+
181
251
  # Get User by id
182
252
  user = client.get_user(users_filtered.first.id)
183
253
  user_mfa = client.get_user(users_filtered2.first.id)
@@ -515,4 +585,4 @@ The gem is available as open source under the terms of the [MIT License](http://
515
585
 
516
586
  ## Code of Conduct
517
587
 
518
- Everyone interacting in the OneLogin Ruby Sdk project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/onelogin/onelogin-ruby-sdk/blob/master/CODE_OF_CONDUCT.md).
588
+ Everyone interacting in the OneLogin Ruby Sdk project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/onelogin/onelogin-ruby-sdk/blob/main/CODE_OF_CONDUCT.md).
@@ -1,19 +1,23 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- httparty (0.18.0)
5
- mime-types (~> 3.0)
4
+ bigdecimal (4.1.2)
5
+ csv (3.3.6)
6
+ httparty (0.24.2)
7
+ csv
8
+ mini_mime (>= 1.0.0)
6
9
  multi_xml (>= 0.5.2)
7
- mime-types (3.3.1)
8
- mime-types-data (~> 3.2015)
9
- mime-types-data (3.2020.0425)
10
- mini_portile2 (2.4.0)
11
- multi_xml (0.6.0)
12
- nokogiri (1.10.9)
13
- mini_portile2 (~> 2.4.0)
14
- onelogin (1.6.0)
10
+ mini_mime (1.1.5)
11
+ mini_portile2 (2.8.9)
12
+ multi_xml (0.9.1)
13
+ bigdecimal (>= 3.1, < 5)
14
+ nokogiri (1.19.4)
15
+ mini_portile2 (~> 2.8.2)
16
+ racc (~> 1.4)
17
+ onelogin (1.7.0)
15
18
  httparty (>= 0.13.7)
16
19
  nokogiri (>= 1.6.3.1)
20
+ racc (1.8.1)
17
21
 
18
22
  PLATFORMS
19
23
  ruby
@@ -22,4 +26,4 @@ DEPENDENCIES
22
26
  onelogin
23
27
 
24
28
  BUNDLED WITH
25
- 2.1.4
29
+ 2.6.9
@@ -52,11 +52,15 @@ OptionParser.new do |opts|
52
52
  end
53
53
  end.parse!
54
54
 
55
+ counter = 0
56
+ limit = options[:limit] || 1000
57
+
55
58
  # Fetch the events
56
59
  client = OneLogin::Api::Client.new(
57
60
  client_id: 'ONELOGIN_CLIENT_ID_GOES_HERE',
58
61
  client_secret: 'ONELOGIN_CLIENT_SECRET_GOES_HERE',
59
- region: 'us'
62
+ region: 'us',
63
+ max_results: limit
60
64
  )
61
65
 
62
66
  attribute_names = ['id', 'created_at', 'account_id', 'user_id', 'user_name', 'event_type_id',
@@ -66,9 +70,6 @@ attribute_names = ['id', 'created_at', 'account_id', 'user_id', 'user_name', 'ev
66
70
  'operation_name', 'directory_sync_run_id', 'directory_id', 'resolution', 'client_id',
67
71
  'resource_type_id', 'error_description', 'risk_score', 'risk_reasons', 'risk_cookie_id', 'browser_fingerprint']
68
72
 
69
- counter = 0
70
- limit = options[:limit] || 1000
71
-
72
73
  # We remove limit from options parsed to the api as we want to fetch
73
74
  # the max number of records possible and then use the cursor that is
74
75
  # built into the ruby sdk to limit the results
@@ -1,6 +1,9 @@
1
1
  module OneLogin
2
2
  module Api
3
3
  class ApiException < Exception
4
+ # HTTP status (or API status code) the exception was raised for.
5
+ attr_reader :code
6
+
4
7
  def initialize(message, code)
5
8
  super(message)
6
9
  @code = code
@@ -21,6 +21,7 @@ module OneLogin
21
21
 
22
22
  attr_accessor :client_id, :client_secret, :region
23
23
  attr_accessor :user_agent, :error, :error_description, :error_attribute
24
+ attr_reader :api_version
24
25
 
25
26
  NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
26
27
  Nokogiri::XML::ParseOptions::NONET
@@ -32,6 +33,9 @@ module OneLogin
32
33
  #
33
34
  # @param config [Hash] Client Id, Client Secret and Region
34
35
  #
36
+ # Pass api_version to call that API version from every method that
37
+ # supports it (see API_VERSIONS). Other methods keep their default.
38
+ #
35
39
  def initialize(config)
36
40
  options = Hash[config.map { |(k, v)| [k.to_sym, v] }]
37
41
 
@@ -40,6 +44,7 @@ module OneLogin
40
44
  @region = options[:region] || 'us'
41
45
  @max_results = options[:max_results] || 1000
42
46
  @token_expiration_buffer = normalize_expiration_buffer(options[:token_expiration_buffer])
47
+ @api_version = normalize_api_version(options[:api_version])
43
48
 
44
49
  if options[:timeout] and defined? self.class.default_timeout
45
50
  self.class.default_timeout options[:timeout]
@@ -60,6 +65,28 @@ module OneLogin
60
65
  raise ArgumentError, 'client_id & client_secret are required' unless @client_id && @client_secret
61
66
  end
62
67
 
68
+ # Normalize caller-supplied parameter hashes to string keys.
69
+ #
70
+ # The README and examples use symbol keys, but the required-parameter
71
+ # checks below look them up as strings. `to_json` already serializes both
72
+ # forms identically, so only the validation ever disagreed.
73
+ #
74
+ # Anything that isn't a Hash becomes an empty one so the required-parameter
75
+ # guards still fire and report the missing attribute, rather than blowing
76
+ # up on has_key? and surfacing as a generic 500.
77
+ #
78
+ # An already-string-keyed hash is returned untouched, so existing callers
79
+ # keep the exact object they passed - including Hash subclasses with their
80
+ # own to_json.
81
+ #
82
+ def stringify_param_keys(params)
83
+ return {} unless params.is_a?(Hash)
84
+ return params if params.keys.all? { |key| key.is_a?(String) }
85
+
86
+ params.each_with_object({}) { |(key, value), out| out[key.to_s] = value }
87
+ end
88
+ private :stringify_param_keys
89
+
63
90
  # Coerce the configured refresh buffer into a non-negative Integer.
64
91
  #
65
92
  # ENV-backed configs hand this over as a String, and a negative buffer
@@ -83,6 +110,40 @@ module OneLogin
83
110
  end
84
111
  private :normalize_expiration_buffer
85
112
 
113
+ # Coerce the configured API version into one the SDK can call.
114
+ #
115
+ # nil leaves every method on its default version. Anything else must be a
116
+ # version at least one method supports, so a typo fails here instead of
117
+ # quietly calling the defaults. Only Integers and Strings convert:
118
+ # Integer() would truncate 1.9 to 1.
119
+ #
120
+ def normalize_api_version(value)
121
+ return nil if value.nil?
122
+
123
+ version = begin
124
+ Integer(value) if value.is_a?(Integer) || value.is_a?(String)
125
+ rescue ArgumentError
126
+ nil
127
+ end
128
+
129
+ supported = API_VERSIONS.values.flatten.uniq.sort
130
+ unless supported.include?(version)
131
+ raise ArgumentError, "api_version must be one of #{supported.join(', ')}, got #{value.inspect}"
132
+ end
133
+
134
+ version
135
+ end
136
+ private :normalize_api_version
137
+
138
+ # The API version a method calls: the configured api_version when the
139
+ # method supports it, otherwise the method's default.
140
+ #
141
+ def api_version_for(method)
142
+ versions = API_VERSIONS.fetch(method)
143
+ versions.include?(@api_version) ? @api_version : versions.first
144
+ end
145
+ private :api_version_for
146
+
86
147
  # Clean any previous error registered at the client.
87
148
  #
88
149
  def clean_error
@@ -373,14 +434,16 @@ module OneLogin
373
434
  prepare_token
374
435
 
375
436
  begin
437
+ version = api_version_for(:get_users)
376
438
  options = {
377
439
  model: OneLogin::Api::Models::User,
378
440
  headers: authorized_headers,
379
441
  max_results: @max_results,
380
- params: params
442
+ params: params,
443
+ api_version: version
381
444
  }
382
445
 
383
- return Cursor.new(self, url_for(GET_USERS_URL), options)
446
+ return Cursor.new(self, url_for(GET_USERS_URL, version: version), options)
384
447
 
385
448
  rescue Exception => e
386
449
  @error = '500'
@@ -409,7 +472,8 @@ module OneLogin
409
472
  return
410
473
  end
411
474
 
412
- url = url_for(GET_USER_URL, user_id)
475
+ version = api_version_for(:get_user)
476
+ url = url_for(GET_USER_URL, user_id, version: version)
413
477
 
414
478
  response = self.class.get(
415
479
  url,
@@ -418,7 +482,9 @@ module OneLogin
418
482
 
419
483
  if response.code == 200
420
484
  json_data = JSON.parse(response.body)
421
- if json_data && json_data['data']
485
+ if version == 2
486
+ return OneLogin::Api::Models::User.new(json_data) if json_data.is_a?(Hash)
487
+ elsif json_data && json_data['data']
422
488
  return OneLogin::Api::Models::User.new(json_data['data'][0])
423
489
  end
424
490
  else
@@ -452,13 +518,15 @@ module OneLogin
452
518
  return
453
519
  end
454
520
 
521
+ version = api_version_for(:get_user_apps)
455
522
  options = {
456
523
  model: OneLogin::Api::Models::App,
457
524
  headers: authorized_headers,
458
- max_results: @max_results
525
+ max_results: @max_results,
526
+ api_version: version
459
527
  }
460
528
 
461
- return Cursor.new(self, url_for(GET_APPS_FOR_USER_URL, user_id), options)
529
+ return Cursor.new(self, url_for(GET_APPS_FOR_USER_URL, user_id, version: version), options)
462
530
 
463
531
  rescue Exception => e
464
532
  @error = '500'
@@ -566,7 +634,8 @@ module OneLogin
566
634
  prepare_token
567
635
 
568
636
  begin
569
- url = url_for(CREATE_USER_URL)
637
+ version = api_version_for(:create_user)
638
+ url = url_for(CREATE_USER_URL, version: version)
570
639
 
571
640
  response = self.class.post(
572
641
  url,
@@ -574,9 +643,12 @@ module OneLogin
574
643
  body: user_params.to_json
575
644
  )
576
645
 
577
- if response.code == 200
646
+ # v1 answers a create with 200, v2 with 201 Created.
647
+ if response.code == (version == 2 ? 201 : 200)
578
648
  json_data = JSON.parse(response.body)
579
- if json_data && json_data['data']
649
+ if version == 2
650
+ return OneLogin::Api::Models::User.new(json_data) if json_data.is_a?(Hash)
651
+ elsif json_data && json_data['data']
580
652
  return OneLogin::Api::Models::User.new(json_data['data'][0])
581
653
  end
582
654
  else
@@ -617,7 +689,8 @@ module OneLogin
617
689
  return
618
690
  end
619
691
 
620
- url = url_for(UPDATE_USER_URL, user_id)
692
+ version = api_version_for(:update_user)
693
+ url = url_for(UPDATE_USER_URL, user_id, version: version)
621
694
 
622
695
  response = self.class.put(
623
696
  url,
@@ -627,7 +700,9 @@ module OneLogin
627
700
 
628
701
  if response.code == 200
629
702
  json_data = JSON.parse(response.body)
630
- if json_data && json_data['data']
703
+ if version == 2
704
+ return OneLogin::Api::Models::User.new(json_data) if json_data.is_a?(Hash)
705
+ elsif json_data && json_data['data']
631
706
  return OneLogin::Api::Models::User.new(json_data['data'][0])
632
707
  end
633
708
  else
@@ -1047,14 +1122,18 @@ module OneLogin
1047
1122
  return
1048
1123
  end
1049
1124
 
1050
- url = url_for(DELETE_USER_URL, user_id)
1125
+ version = api_version_for(:delete_user)
1126
+ url = url_for(DELETE_USER_URL, user_id, version: version)
1051
1127
 
1052
1128
  response = self.class.delete(
1053
1129
  url,
1054
1130
  headers: authorized_headers
1055
1131
  )
1056
1132
 
1057
- if response.code == 200
1133
+ # v2 confirms a delete with an empty 204 rather than a status body.
1134
+ if version == 2 && response.code == 204
1135
+ return true
1136
+ elsif version == 1 && response.code == 200
1058
1137
  return handle_operation_response(response)
1059
1138
  else
1060
1139
  @error = response.code.to_s
@@ -1141,6 +1220,8 @@ module OneLogin
1141
1220
  begin
1142
1221
  url = url_for(SESSION_LOGIN_TOKEN_URL)
1143
1222
 
1223
+ query_params = stringify_param_keys(query_params)
1224
+
1144
1225
  if query_params.nil? || !query_params.has_key?('username_or_email') || !query_params.has_key?('password') || !query_params.has_key?('subdomain')
1145
1226
  raise "username_or_email, password and subdomain are required parameters"
1146
1227
  end
@@ -1366,7 +1447,12 @@ module OneLogin
1366
1447
  begin
1367
1448
  url = url_for(CREATE_APP_URL)
1368
1449
 
1369
- unless app_params.has_key?('connector_id') || app_params['connector_id'].to_s.empty?
1450
+ app_params = stringify_param_keys(app_params)
1451
+
1452
+ # Was `unless has_key? || value.empty?`, which is true in every case,
1453
+ # so the check never fired and an app could be created with no
1454
+ # connector_id at all.
1455
+ if !app_params.has_key?('connector_id') || app_params['connector_id'].to_s.empty?
1370
1456
  @error = '400'
1371
1457
  @error_description = "connector_id is required"
1372
1458
  @error_attribute = "connector_id"
@@ -1592,14 +1678,16 @@ module OneLogin
1592
1678
  prepare_token
1593
1679
 
1594
1680
  begin
1681
+ version = api_version_for(:get_roles)
1595
1682
  options = {
1596
1683
  model: OneLogin::Api::Models::Role,
1597
1684
  headers: authorized_headers,
1598
1685
  max_results: @max_results,
1599
- params: params
1686
+ params: params,
1687
+ api_version: version
1600
1688
  }
1601
1689
 
1602
- return Cursor.new(self, url_for(GET_ROLES_URL), options)
1690
+ return Cursor.new(self, url_for(GET_ROLES_URL, version: version), options)
1603
1691
 
1604
1692
  rescue Exception => e
1605
1693
  @error = '500'
@@ -1628,7 +1716,8 @@ module OneLogin
1628
1716
  return
1629
1717
  end
1630
1718
 
1631
- url = url_for(GET_ROLE_URL, role_id)
1719
+ version = api_version_for(:get_role)
1720
+ url = url_for(GET_ROLE_URL, role_id, version: version)
1632
1721
 
1633
1722
  response = self.class.get(
1634
1723
  url,
@@ -1637,7 +1726,9 @@ module OneLogin
1637
1726
 
1638
1727
  if response.code == 200
1639
1728
  json_data = JSON.parse(response.body)
1640
- if json_data && json_data['data']
1729
+ if version == 2
1730
+ return OneLogin::Api::Models::Role.new(json_data) if json_data.is_a?(Hash)
1731
+ elsif json_data && json_data['data']
1641
1732
  return OneLogin::Api::Models::Role.new(json_data['data'][0])
1642
1733
  end
1643
1734
  else
@@ -25,6 +25,7 @@ class Cursor
25
25
  @params = options[:params] || {}
26
26
  @max_results = options[:max_results]
27
27
  @container = options[:container] || 'data'
28
+ @api_version = options[:api_version]
28
29
 
29
30
  @collection = []
30
31
  @after_cursor = options.fetch(:after_cursor, nil)
@@ -53,19 +54,24 @@ class Cursor
53
54
  private
54
55
 
55
56
  def fetch_next_page
57
+ return fetch_next_v2_page if @api_version == 2
58
+
56
59
  @params = @params.merge(after_cursor: @after_cursor) if @after_cursor
57
60
  @client.prepare_token
58
61
 
59
62
  response = @client.class.get(
60
63
  @url,
61
- headers: @headers,
64
+ headers: request_headers,
62
65
  query: @params
63
66
  )
64
67
 
65
68
  json = response.parsed_response
66
69
 
67
- if json.nil?
68
- raise OneLogin::Api::ApiException.new("Response could not be parsed", 500)
70
+ # HTTParty picks its parser from Content-Type, so a gateway or proxy error
71
+ # page comes back as a String and every has_key? below would blow up with a
72
+ # NoMethodError instead of a usable ApiException.
73
+ if json.nil? || !json.is_a?(Hash)
74
+ raise OneLogin::Api::ApiException.new("Response could not be parsed", response.code)
69
75
  elsif !json.has_key?(@container) && json.has_key?('status') && json["status"]["error"] == true
70
76
  raise OneLogin::Api::ApiException.new(extract_error_message_from_response(response), json["status"]["code"])
71
77
  elsif !json.has_key?(@container) && json.has_key?('statusCode')
@@ -88,6 +94,43 @@ class Cursor
88
94
  end
89
95
  end
90
96
 
97
+ # v2 returns a bare array and puts the next page's cursor in the After-Cursor
98
+ # header, which is absent on the last page. The cursor already encodes the
99
+ # filters and page size, and the API rejects a cursor sent alongside limit or
100
+ # page, so follow-up requests send the cursor alone.
101
+ def fetch_next_v2_page
102
+ @client.prepare_token
103
+
104
+ response = @client.class.get(
105
+ @url,
106
+ headers: request_headers,
107
+ query: @after_cursor ? { cursor: @after_cursor } : @params
108
+ )
109
+
110
+ results = response.parsed_response
111
+
112
+ unless response.code == 200 && results.is_a?(Array)
113
+ message = results.is_a?(Hash) ? extract_error_message_from_response(response) : "Response could not be parsed"
114
+ raise OneLogin::Api::ApiException.new(message, response.code)
115
+ end
116
+
117
+ @collection += if results_remaining < results.size
118
+ results.slice(0, results_remaining)
119
+ else
120
+ results
121
+ end
122
+
123
+ @after_cursor = response.headers['after-cursor']
124
+ @last_cursor_empty = @after_cursor.nil? || @after_cursor.empty?
125
+ end
126
+
127
+ # The headers were built when the cursor was, and prepare_token may have
128
+ # refreshed the token since: a cursor enumerated later, or one whose pages
129
+ # outlive the token. Take the current token from the client.
130
+ def request_headers
131
+ @headers.merge(@client.authorized_headers)
132
+ end
133
+
91
134
  def after_cursor(json)
92
135
  value = nil
93
136
  if json.has_key?('pagination')
@@ -6,15 +6,22 @@ module OneLogin
6
6
 
7
7
  attr_accessor :id, :name, :icon, :provisioned
8
8
  attr_accessor :extension, :login_id, :personal
9
+ attr_accessor :provisioning_enabled, :provisioning_state, :provisioning_status
9
10
 
11
+ # v2 renames icon to icon_url and replaces provisioned and personal with
12
+ # the provisioning_* fields. Its login_id is the numeric id of the
13
+ # user's login, where v1 gives the username used to sign in.
10
14
  def initialize(data)
11
15
  @id = data['id']
12
16
  @name = data['name']
13
- @icon = data['icon']
17
+ @icon = data['icon'] || data['icon_url']
14
18
  @provisioned = data['provisioned']
15
19
  @extension = data['extension']
16
20
  @login_id = data['login_id']
17
21
  @personal = data['personal']
22
+ @provisioning_enabled = data['provisioning_enabled']
23
+ @provisioning_state = data['provisioning_state']
24
+ @provisioning_status = data['provisioning_status']
18
25
  end
19
26
  end
20
27
  end
@@ -28,7 +28,8 @@ module OneLogin
28
28
  @samaccountname = data['samaccountname'].to_s
29
29
  @userprincipalname = data['userprincipalname'].to_s
30
30
  @group_id = data['group_id']? data['group_id'].to_i : nil
31
- @role_ids = data['role_id']? data['role_id'] : []
31
+ # v1 calls it role_id, v2 role_ids.
32
+ @role_ids = data['role_id'] || data['role_ids'] || []
32
33
  @custom_attributes = data['custom_attributes'] ? data['custom_attributes'] : []
33
34
  @openid_name = data['openid_name'].to_s
34
35
  @locale_code = data['locale_code'].to_s
@@ -98,6 +98,24 @@ module OneLogin
98
98
  GET_USERS_ASSIGNED_TO_PRIVILEGE_URL = "https://api.%s.onelogin.com/api/1/privileges/%s/users"
99
99
  ASSIGN_USERS_TO_PRIVILEGE_URL = "https://api.%s.onelogin.com/api/1/privileges/%s/users"
100
100
  REMOVE_USER_FROM_PRIVILEGE_URL = "https://api.%s.onelogin.com/api/1/privileges/%s/users/%s"
101
+
102
+ # API versions each client method can call, keyed by method name.
103
+ #
104
+ # The first version is the default and matches the version in the
105
+ # method's URL constant. A method that isn't listed only calls the
106
+ # version in its URL constant. List a version here only once the method
107
+ # can parse that version's response: v1 and v2 use different envelopes.
108
+ API_VERSIONS = {
109
+ get_users: [1, 2],
110
+ get_user: [1, 2],
111
+ get_user_apps: [1, 2],
112
+ create_user: [1, 2],
113
+ update_user: [1, 2],
114
+ delete_user: [1, 2],
115
+ get_roles: [1, 2],
116
+ get_role: [1, 2]
117
+ }.transform_values(&:freeze).freeze
118
+
101
119
  VALID_ACTIONS = [
102
120
  "apps:List",
103
121
  "apps:Get",
@@ -6,7 +6,9 @@ module OneLogin
6
6
  message = ''
7
7
  content = JSON.parse(response.body)
8
8
  if content
9
- if content.has_key?('status')
9
+ # v1 nests the error under a status object. Some v2 endpoints send
10
+ # status as the bare HTTP code, with the text in description.
11
+ if content['status'].is_a?(Hash)
10
12
  status = content['status']
11
13
  if status.has_key?('message')
12
14
  if status['message'].instance_of?(Hash)
@@ -21,6 +23,8 @@ module OneLogin
21
23
  end
22
24
  elsif content.has_key?('message')
23
25
  message = content['message']
26
+ elsif content.has_key?('description')
27
+ message = content['description']
24
28
  elsif content.has_key?('name')
25
29
  message = content['name']
26
30
  end
@@ -31,7 +35,7 @@ module OneLogin
31
35
  def extract_error_attribute_from_response(response)
32
36
  attribute = nil
33
37
  content = JSON.parse(response.body)
34
- if content && content.has_key?('status')
38
+ if content && content['status'].is_a?(Hash)
35
39
  status = content['status']
36
40
  if status.has_key?('message') && status['message'].instance_of?(Hash)
37
41
  if status['message'].has_key?('attribute')