onelogin 1.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 341fedcf1ef3faf8e076b127ad2b7379c48fe04957396d850d3468a01a27b369
4
- data.tar.gz: ca91b881e24411ff62d8d91b37e6226d5f879c8f245eaced58c44d84194ff892
3
+ metadata.gz: '0887ddcbf97b6c873d5547842d27822f35b7e4eeefa4c78285ed2828c0cf546b'
4
+ data.tar.gz: 5a59b6e4c2ff4605775a8473931ed82183d9f434d455a559214828cd612c1988
5
5
  SHA512:
6
- metadata.gz: 6d0a9ac12d1bb6fc17434ff5503f0fe311ad9b48603f430244423c19c6b9e130e434e2892f4d3308dcfa4c1dc966a5557ba6537d7a733567dd5f42e3a6db8b49
7
- data.tar.gz: c95b20d2915d751098950bc1bda5b0356bbc1d9bb4fad76f3c94bf4a8134705bbb3c745b1b4518e849cb0c1994ca2c3a0a3f7584c8eab051dd1344f4dee995c4
6
+ metadata.gz: 01b0e196b7f431a2d6e04a1b64757a4b0224b0a5b34e3b17ee6c829f642e6b321305d6ebe15dee6dae44fd15b92f64555927522994c469f7f8ba2a1964ccfb10
7
+ data.tar.gz: 2595a8b6f36f9a446e991baa18b66e37c9f2ac21687a43d7ca6f0c1dfa1b5c515a7edf8c9d57f3e64874d7690d79d807af31c3f689842fc21fda936bd8fd2b52
data/README.md CHANGED
@@ -59,6 +59,7 @@ If you don't have an account you can [sign up for a free developer account here]
59
59
  |max_results| Optional: Defaults to 1000 |
60
60
  |timeout| Optional: Defaults to 60 (requires httparty > 0.16.2) |
61
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) |
62
63
 
63
64
  ```ruby
64
65
  require 'onelogin'
@@ -158,6 +159,34 @@ client = OneLogin::Api::Client.new(
158
159
  client.get_events.map {|event| event.id}
159
160
  ```
160
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
+
161
190
 
162
191
 
163
192
  ### Available Methods
@@ -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]
@@ -105,6 +110,40 @@ module OneLogin
105
110
  end
106
111
  private :normalize_expiration_buffer
107
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
+
108
147
  # Clean any previous error registered at the client.
109
148
  #
110
149
  def clean_error
@@ -395,14 +434,16 @@ module OneLogin
395
434
  prepare_token
396
435
 
397
436
  begin
437
+ version = api_version_for(:get_users)
398
438
  options = {
399
439
  model: OneLogin::Api::Models::User,
400
440
  headers: authorized_headers,
401
441
  max_results: @max_results,
402
- params: params
442
+ params: params,
443
+ api_version: version
403
444
  }
404
445
 
405
- return Cursor.new(self, url_for(GET_USERS_URL), options)
446
+ return Cursor.new(self, url_for(GET_USERS_URL, version: version), options)
406
447
 
407
448
  rescue Exception => e
408
449
  @error = '500'
@@ -431,7 +472,8 @@ module OneLogin
431
472
  return
432
473
  end
433
474
 
434
- 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)
435
477
 
436
478
  response = self.class.get(
437
479
  url,
@@ -440,7 +482,9 @@ module OneLogin
440
482
 
441
483
  if response.code == 200
442
484
  json_data = JSON.parse(response.body)
443
- 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']
444
488
  return OneLogin::Api::Models::User.new(json_data['data'][0])
445
489
  end
446
490
  else
@@ -474,13 +518,15 @@ module OneLogin
474
518
  return
475
519
  end
476
520
 
521
+ version = api_version_for(:get_user_apps)
477
522
  options = {
478
523
  model: OneLogin::Api::Models::App,
479
524
  headers: authorized_headers,
480
- max_results: @max_results
525
+ max_results: @max_results,
526
+ api_version: version
481
527
  }
482
528
 
483
- 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)
484
530
 
485
531
  rescue Exception => e
486
532
  @error = '500'
@@ -588,7 +634,8 @@ module OneLogin
588
634
  prepare_token
589
635
 
590
636
  begin
591
- url = url_for(CREATE_USER_URL)
637
+ version = api_version_for(:create_user)
638
+ url = url_for(CREATE_USER_URL, version: version)
592
639
 
593
640
  response = self.class.post(
594
641
  url,
@@ -596,9 +643,12 @@ module OneLogin
596
643
  body: user_params.to_json
597
644
  )
598
645
 
599
- if response.code == 200
646
+ # v1 answers a create with 200, v2 with 201 Created.
647
+ if response.code == (version == 2 ? 201 : 200)
600
648
  json_data = JSON.parse(response.body)
601
- 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']
602
652
  return OneLogin::Api::Models::User.new(json_data['data'][0])
603
653
  end
604
654
  else
@@ -639,7 +689,8 @@ module OneLogin
639
689
  return
640
690
  end
641
691
 
642
- 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)
643
694
 
644
695
  response = self.class.put(
645
696
  url,
@@ -649,7 +700,9 @@ module OneLogin
649
700
 
650
701
  if response.code == 200
651
702
  json_data = JSON.parse(response.body)
652
- 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']
653
706
  return OneLogin::Api::Models::User.new(json_data['data'][0])
654
707
  end
655
708
  else
@@ -1069,14 +1122,18 @@ module OneLogin
1069
1122
  return
1070
1123
  end
1071
1124
 
1072
- 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)
1073
1127
 
1074
1128
  response = self.class.delete(
1075
1129
  url,
1076
1130
  headers: authorized_headers
1077
1131
  )
1078
1132
 
1079
- 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
1080
1137
  return handle_operation_response(response)
1081
1138
  else
1082
1139
  @error = response.code.to_s
@@ -1621,14 +1678,16 @@ module OneLogin
1621
1678
  prepare_token
1622
1679
 
1623
1680
  begin
1681
+ version = api_version_for(:get_roles)
1624
1682
  options = {
1625
1683
  model: OneLogin::Api::Models::Role,
1626
1684
  headers: authorized_headers,
1627
1685
  max_results: @max_results,
1628
- params: params
1686
+ params: params,
1687
+ api_version: version
1629
1688
  }
1630
1689
 
1631
- return Cursor.new(self, url_for(GET_ROLES_URL), options)
1690
+ return Cursor.new(self, url_for(GET_ROLES_URL, version: version), options)
1632
1691
 
1633
1692
  rescue Exception => e
1634
1693
  @error = '500'
@@ -1657,7 +1716,8 @@ module OneLogin
1657
1716
  return
1658
1717
  end
1659
1718
 
1660
- 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)
1661
1721
 
1662
1722
  response = self.class.get(
1663
1723
  url,
@@ -1666,7 +1726,9 @@ module OneLogin
1666
1726
 
1667
1727
  if response.code == 200
1668
1728
  json_data = JSON.parse(response.body)
1669
- 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']
1670
1732
  return OneLogin::Api::Models::Role.new(json_data['data'][0])
1671
1733
  end
1672
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,12 +54,14 @@ 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
 
@@ -91,6 +94,43 @@ class Cursor
91
94
  end
92
95
  end
93
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
+
94
134
  def after_cursor(json)
95
135
  value = nil
96
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')
@@ -3,13 +3,20 @@ module OneLogin
3
3
  module Util
4
4
  module UrlBuilder
5
5
 
6
+ # The version segment of a versioned endpoint, e.g. "/api/1/"
7
+ API_VERSION_SEGMENT = %r{/api/\d+/}
8
+
6
9
  # Build the URL of the API endpoint
7
10
  #
8
11
  # @param base [String] Base of the endpoint
9
12
  # @param obj_id [String, nil] Id of the referenced object
10
13
  # @param extra_id [String, nil] Id of the secundary referenced object
14
+ # @param version [Integer, nil] API version to call. Defaults to the
15
+ # version already in the base.
11
16
  #
12
- def url_for(base, obj_id=nil, extra_id=nil)
17
+ def url_for(base, obj_id=nil, extra_id=nil, version: nil)
18
+ base = with_api_version(base, version) unless version.nil?
19
+
13
20
  if obj_id.nil? || obj_id.to_s.empty?
14
21
  base % [@region]
15
22
  elsif extra_id.nil? || extra_id.to_s.empty?
@@ -18,6 +25,23 @@ module OneLogin
18
25
  base % [@region, obj_id, extra_id]
19
26
  end
20
27
  end
28
+
29
+ private
30
+
31
+ # Swap the version segment of an endpoint, so the URL constants keep
32
+ # their current values and stay the default.
33
+ #
34
+ # Raises rather than calling a version nobody asked for. The client
35
+ # still rescues this into error '500', but error_description then names
36
+ # the endpoint.
37
+ #
38
+ def with_api_version(base, version)
39
+ unless base.scan(API_VERSION_SEGMENT).size == 1
40
+ raise ArgumentError, "#{base} has no single API version segment to replace"
41
+ end
42
+
43
+ base.sub(API_VERSION_SEGMENT, "/api/#{version}/")
44
+ end
21
45
  end
22
46
  end
23
47
  end
@@ -1,3 +1,3 @@
1
1
  module OneLogin
2
- VERSION = "1.8.0"
2
+ VERSION = "1.9.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: onelogin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OneLogin
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-08-04 00:00:00.000000000 Z
10
+ date: 2026-10-23 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: httparty
@@ -167,7 +166,6 @@ licenses:
167
166
  - MIT
168
167
  metadata:
169
168
  allowed_push_host: https://rubygems.org
170
- post_install_message:
171
169
  rdoc_options: []
172
170
  require_paths:
173
171
  - lib
@@ -182,8 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
180
  - !ruby/object:Gem::Version
183
181
  version: '0'
184
182
  requirements: []
185
- rubygems_version: 3.2.33
186
- signing_key:
183
+ rubygems_version: 3.6.2
187
184
  specification_version: 4
188
185
  summary: OneLogin's Ruby SDK.
189
186
  test_files: []