workos 0.11.2 → 1.3.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.semaphore/semaphore.yml +2 -2
  4. data/Gemfile.lock +2 -2
  5. data/LICENSE +1 -1
  6. data/lib/workos.rb +2 -0
  7. data/lib/workos/client.rb +3 -2
  8. data/lib/workos/directory.rb +4 -1
  9. data/lib/workos/directory_user.rb +6 -1
  10. data/lib/workos/errors.rb +13 -2
  11. data/lib/workos/organizations.rb +171 -0
  12. data/lib/workos/portal.rb +0 -133
  13. data/lib/workos/profile.rb +8 -10
  14. data/lib/workos/profile_and_token.rb +28 -0
  15. data/lib/workos/sso.rb +31 -100
  16. data/lib/workos/types/directory_struct.rb +1 -0
  17. data/lib/workos/types/directory_user_struct.rb +1 -0
  18. data/lib/workos/version.rb +1 -1
  19. data/spec/lib/workos/organizations_spec.rb +191 -0
  20. data/spec/lib/workos/portal_spec.rb +0 -160
  21. data/spec/lib/workos/sso_spec.rb +41 -122
  22. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_after.yml +12 -9
  23. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_before.yml +8 -5
  24. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_domain.yml +8 -8
  25. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_limit.yml +9 -9
  26. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_no_options.yml +23 -10
  27. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_search.yml +8 -8
  28. data/spec/support/fixtures/vcr_cassettes/organization/delete.yml +72 -0
  29. data/spec/support/fixtures/vcr_cassettes/{sso/create_connection_with_invalid_source.yml → organization/delete_invalid.yml} +26 -12
  30. data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +74 -0
  31. data/workos.gemspec +1 -1
  32. metadata +15 -9
  33. data/spec/support/fixtures/vcr_cassettes/sso/create_connection_with_valid_source.yml +0 -63
@@ -48,22 +48,20 @@ module WorkOS
48
48
 
49
49
  private
50
50
 
51
- # rubocop:disable Metrics/AbcSize
52
51
  sig { params(json_string: String).returns(WorkOS::Types::ProfileStruct) }
53
52
  def parse_json(json_string)
54
53
  hash = JSON.parse(json_string, symbolize_names: true)
55
54
 
56
55
  WorkOS::Types::ProfileStruct.new(
57
- id: hash[:profile][:id],
58
- email: hash[:profile][:email],
59
- first_name: hash[:profile][:first_name],
60
- last_name: hash[:profile][:last_name],
61
- connection_id: hash[:profile][:connection_id],
62
- connection_type: hash[:profile][:connection_type],
63
- idp_id: hash[:profile][:idp_id],
64
- raw_attributes: hash[:profile][:raw_attributes],
56
+ id: hash[:id],
57
+ email: hash[:email],
58
+ first_name: hash[:first_name],
59
+ last_name: hash[:last_name],
60
+ connection_id: hash[:connection_id],
61
+ connection_type: hash[:connection_type],
62
+ idp_id: hash[:idp_id],
63
+ raw_attributes: hash[:raw_attributes],
65
64
  )
66
65
  end
67
- # rubocop:enable Metrics/AbcSize
68
66
  end
69
67
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module WorkOS
5
+ # The ProfileAndToken class represents a Profile and a corresponding
6
+ # Access Token. This class is not meant to be instantiated in user space, and
7
+ # is instantiated internally but exposed.
8
+ class ProfileAndToken
9
+ extend T::Sig
10
+
11
+ attr_accessor :access_token, :profile
12
+
13
+ sig { params(profile_and_token_json: String).void }
14
+ def initialize(profile_and_token_json)
15
+ json = JSON.parse(profile_and_token_json, symbolize_names: true)
16
+
17
+ @access_token = T.let(json[:access_token], String)
18
+ @profile = WorkOS::Profile.new(json[:profile].to_json)
19
+ end
20
+
21
+ def to_json(*)
22
+ {
23
+ access_token: access_token,
24
+ profile: profile.to_json,
25
+ }
26
+ end
27
+ end
28
+ end
data/lib/workos/sso.rb CHANGED
@@ -30,8 +30,6 @@ module WorkOS
30
30
  # WorkOS.
31
31
  # @param [String] client_id The WorkOS client ID for the environment
32
32
  # where you've configured your SSO connection.
33
- # @param [String] project_id The WorkOS project ID for the project.
34
- # The project_id is deprecated in Dashboard2.
35
33
  # @param [String] redirect_uri The URI where users are directed
36
34
  # after completing the authentication step. Must match a
37
35
  # configured redirect URI on your WorkOS dashboard.
@@ -56,7 +54,6 @@ module WorkOS
56
54
  sig do
57
55
  params(
58
56
  redirect_uri: String,
59
- project_id: T.nilable(String),
60
57
  client_id: T.nilable(String),
61
58
  domain: T.nilable(String),
62
59
  provider: T.nilable(String),
@@ -64,22 +61,14 @@ module WorkOS
64
61
  state: T.nilable(String),
65
62
  ).returns(String)
66
63
  end
67
- # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
68
64
  def authorization_url(
69
65
  redirect_uri:,
70
- project_id: nil,
71
66
  client_id: nil,
72
67
  domain: nil,
73
68
  provider: nil,
74
69
  connection: nil,
75
70
  state: ''
76
71
  )
77
- if project_id
78
- warn '[DEPRECATION] `project_id` is deprecated.
79
- Please use `client_id` instead.'
80
- client_id = project_id
81
- end
82
-
83
72
  validate_authorization_url_arguments(
84
73
  provider: provider,
85
74
  domain: domain,
@@ -98,46 +87,38 @@ module WorkOS
98
87
 
99
88
  "https://#{WorkOS::API_HOSTNAME}/sso/authorize?#{query}"
100
89
  end
101
- # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
90
+
91
+ sig do
92
+ params(
93
+ access_token: String,
94
+ ).returns(WorkOS::Profile)
95
+ end
96
+ def get_profile(access_token:)
97
+ response = execute_request(
98
+ request: get_request(
99
+ path: '/sso/profile',
100
+ auth: true,
101
+ access_token: access_token,
102
+ ),
103
+ )
104
+
105
+ WorkOS::Profile.new(response.body)
106
+ end
102
107
 
103
108
  # Fetch the profile details for the authenticated SSO user.
104
109
  #
105
110
  # @param [String] code The authorization code provided in the callback URL
106
111
  # @param [String] client_id The WorkOS client ID for the environment
107
- # where you've configured your SSO connection
108
- # @param [String] project_id The WorkOS project ID for the project.
109
- # The project_id is deprecated in Dashboard2.
112
+ # where you've configured your SSO connection
110
113
  #
111
- # @example
112
- # WorkOS::SSO.profile(
113
- # code: 'acme.com',
114
- # client_id: 'project_01DG5TGK363GRVXP3ZS40WNGEZ'
115
- # )
116
- # => #<WorkOS::Profile:0x00007fb6e4193d20
117
- # @id="prof_01DRA1XNSJDZ19A31F183ECQW5",
118
- # @email="demo@workos-okta.com",
119
- # @first_name="WorkOS",
120
- # @connection_type="OktaSAML",
121
- # @last_name="Demo",
122
- # @idp_id="00u1klkowm8EGah2H357",
123
- # @access_token="01DVX6QBS3EG6FHY2ESAA5Q65X"
124
- # >
125
- #
126
- # @return [WorkOS::Profile]
114
+ # @return [WorkOS::ProfileAndToken]
127
115
  sig do
128
116
  params(
129
117
  code: String,
130
- project_id: T.nilable(String),
131
118
  client_id: T.nilable(String),
132
- ).returns(WorkOS::Profile)
119
+ ).returns(WorkOS::ProfileAndToken)
133
120
  end
134
- def profile(code:, project_id: nil, client_id: nil)
135
- if project_id
136
- warn '[DEPRECATION] `project_id` is deprecated.
137
- Please use `client_id` instead.'
138
- client_id = project_id
139
- end
140
-
121
+ def profile_and_token(code:, client_id: nil)
141
122
  body = {
142
123
  client_id: client_id,
143
124
  client_secret: WorkOS.key!,
@@ -146,65 +127,9 @@ module WorkOS
146
127
  }
147
128
 
148
129
  response = client.request(post_request(path: '/sso/token', body: body))
149
- check_and_raise_profile_error(response: response)
130
+ check_and_raise_profile_and_token_error(response: response)
150
131
 
151
- WorkOS::Profile.new(response.body)
152
- end
153
-
154
- # Promote a DraftConnection created via the WorkOS.js embed such that the
155
- # Enterprise users can begin signing into your application.
156
- #
157
- # @param [String] token The Draft Connection token that's been provided to
158
- # you by the WorkOS.js
159
- #
160
- # @example
161
- # WorkOS::SSO.promote_draft_connection(
162
- # token: 'draft_conn_429u59js',
163
- # )
164
- # => true
165
- #
166
- # @return [Bool] - returns `true` if successful, `false` otherwise.
167
- # @see https://github.com/workos-inc/ruby-idp-link-example
168
- sig { params(token: String).returns(T::Boolean) }
169
- def promote_draft_connection(token:)
170
- request = post_request(
171
- auth: true,
172
- path: "/draft_connections/#{token}/activate",
173
- )
174
-
175
- response = client.request(request)
176
-
177
- response.is_a? Net::HTTPSuccess
178
- end
179
-
180
- # Create a Connection
181
- #
182
- # @param [String] source The Draft Connection token that's been provided
183
- # to you by WorkOS.js
184
- #
185
- # @example
186
- # WorkOS::SSO.create_connection(source: 'draft_conn_429u59js')
187
- # => #<WorkOS::Connection:0x00007fb6e4193d20
188
- # @id="conn_02DRA1XNSJDZ19A31F183ECQW9",
189
- # @name="Foo Corp",
190
- # @connection_type="OktaSAML",
191
- # @domains=
192
- # [{:object=>"connection_domain",
193
- # :id=>"domain_01E6PK9N3XMD8RHWF7S66380AR",
194
- # :domain=>"example.com"}]>
195
- #
196
- # @return [WorkOS::Connection]
197
- sig { params(source: String).returns(WorkOS::Connection) }
198
- def create_connection(source:)
199
- request = post_request(
200
- auth: true,
201
- path: '/connections',
202
- body: { source: source },
203
- )
204
-
205
- response = execute_request(request: request)
206
-
207
- WorkOS::Connection.new(response.body)
132
+ WorkOS::ProfileAndToken.new(response.body)
208
133
  end
209
134
 
210
135
  # Retrieve connections.
@@ -323,12 +248,15 @@ module WorkOS
323
248
  end
324
249
 
325
250
  sig { params(response: Net::HTTPResponse).void }
326
- def check_and_raise_profile_error(response:)
251
+ # rubocop:disable Metrics/MethodLength
252
+ def check_and_raise_profile_and_token_error(response:)
327
253
  begin
328
254
  body = JSON.parse(response.body)
329
- return if body['profile']
255
+ return if body['access_token'] && body['profile']
330
256
 
331
257
  message = body['message']
258
+ error = body['error']
259
+ error_description = body['error_description']
332
260
  request_id = response['x-request-id']
333
261
  rescue StandardError
334
262
  message = 'Something went wrong'
@@ -336,10 +264,13 @@ module WorkOS
336
264
 
337
265
  raise APIError.new(
338
266
  message: message,
267
+ error: error,
268
+ error_description: error_description,
339
269
  http_status: nil,
340
270
  request_id: request_id,
341
271
  )
342
272
  end
273
+ # rubocop:enable Metrics/MethodLength
343
274
  end
344
275
  end
345
276
  end
@@ -11,6 +11,7 @@ module WorkOS
11
11
  const :domain, String
12
12
  const :type, String
13
13
  const :state, String
14
+ const :organization_id, String
14
15
  end
15
16
  end
16
17
  end
@@ -13,6 +13,7 @@ module WorkOS
13
13
  const :last_name, T.nilable(String)
14
14
  const :username, T.nilable(String)
15
15
  const :state, T.nilable(String)
16
+ const :groups, T::Array[T.untyped]
16
17
  const :raw_attributes, T::Hash[Symbol, Object]
17
18
  end
18
19
  end
@@ -2,5 +2,5 @@
2
2
  # typed: strong
3
3
 
4
4
  module WorkOS
5
- VERSION = '0.11.2'
5
+ VERSION = '1.3.0'
6
6
  end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ describe WorkOS::Organizations do
5
+ describe '.create_organization' do
6
+ context 'with valid payload' do
7
+ it 'creates an organization' do
8
+ VCR.use_cassette 'organization/create' do
9
+ organization = described_class.create_organization(
10
+ domains: ['example.com'],
11
+ name: 'Test Organization',
12
+ )
13
+
14
+ expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T')
15
+ expect(organization.name).to eq('Test Organization')
16
+ expect(organization.domains.first[:domain]).to eq('example.com')
17
+ end
18
+ end
19
+ end
20
+
21
+ context 'with an invalid payload' do
22
+ it 'returns an error' do
23
+ VCR.use_cassette 'organization/create_invalid' do
24
+ expect do
25
+ described_class.create_organization(
26
+ domains: ['example.com'],
27
+ name: 'Test Organization 2',
28
+ )
29
+ end.to raise_error(
30
+ WorkOS::APIError,
31
+ /An Organization with the domain example.com already exists/,
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '.list_organizations' do
39
+ context 'with no options' do
40
+ it 'returns organizations and metadata' do
41
+ expected_metadata = {
42
+ 'after' => nil,
43
+ 'before' => 'before-id',
44
+ }
45
+
46
+ VCR.use_cassette 'organization/list' do
47
+ organizations = described_class.list_organizations
48
+
49
+ expect(organizations.data.size).to eq(7)
50
+ expect(organizations.list_metadata).to eq(expected_metadata)
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'with the before option' do
56
+ it 'forms the proper request to the API' do
57
+ request_args = [
58
+ '/organizations?before=before-id',
59
+ 'Content-Type' => 'application/json'
60
+ ]
61
+
62
+ expected_request = Net::HTTP::Get.new(*request_args)
63
+
64
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
65
+ and_return(expected_request)
66
+
67
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
68
+ organizations = described_class.list_organizations(
69
+ before: 'before-id',
70
+ )
71
+
72
+ expect(organizations.data.size).to eq(7)
73
+ end
74
+ end
75
+ end
76
+
77
+ context 'with the after option' do
78
+ it 'forms the proper request to the API' do
79
+ request_args = [
80
+ '/organizations?after=after-id',
81
+ 'Content-Type' => 'application/json'
82
+ ]
83
+
84
+ expected_request = Net::HTTP::Get.new(*request_args)
85
+
86
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
87
+ and_return(expected_request)
88
+
89
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
90
+ organizations = described_class.list_organizations(after: 'after-id')
91
+
92
+ expect(organizations.data.size).to eq(7)
93
+ end
94
+ end
95
+ end
96
+
97
+ context 'with the limit option' do
98
+ it 'forms the proper request to the API' do
99
+ request_args = [
100
+ '/organizations?limit=10',
101
+ 'Content-Type' => 'application/json'
102
+ ]
103
+
104
+ expected_request = Net::HTTP::Get.new(*request_args)
105
+
106
+ expect(Net::HTTP::Get).to receive(:new).with(*request_args).
107
+ and_return(expected_request)
108
+
109
+ VCR.use_cassette 'organization/list', match_requests_on: [:path] do
110
+ organizations = described_class.list_organizations(limit: 10)
111
+
112
+ expect(organizations.data.size).to eq(7)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ describe '.get_organization' do
119
+ context 'with a valid id' do
120
+ it 'gets the organization details' do
121
+ VCR.use_cassette('organization/get') do
122
+ organization = described_class.get_organization(
123
+ id: 'org_01EZDF20TZEJXKPSX2BJRN6TV6',
124
+ )
125
+
126
+ expect(organization.id).to eq('org_01EZDF20TZEJXKPSX2BJRN6TV6')
127
+ expect(organization.name).to eq('Foo Corp')
128
+ expect(organization.domains.first[:domain]).to eq('foo-corp.com')
129
+ end
130
+ end
131
+ end
132
+
133
+ context 'with an invalid id' do
134
+ it 'raises an error' do
135
+ VCR.use_cassette('organization/get_invalid') do
136
+ expect do
137
+ described_class.get_organization(id: 'invalid')
138
+ end.to raise_error(
139
+ WorkOS::APIError,
140
+ 'Status 404, Not Found - request ID: ',
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '.update_organization' do
148
+ context 'with valid payload' do
149
+ it 'creates an organization' do
150
+ VCR.use_cassette 'organization/update' do
151
+ organization = described_class.update_organization(
152
+ organization: 'org_01F29YJ068E52HGEB8ZQGC9MJG',
153
+ domains: ['example.me'],
154
+ name: 'Test Organization',
155
+ )
156
+
157
+ expect(organization.id).to eq('org_01F29YJ068E52HGEB8ZQGC9MJG')
158
+ expect(organization.name).to eq('Test Organization')
159
+ expect(organization.domains.first[:domain]).to eq('example.me')
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ describe '.delete_organization' do
166
+ context 'with a valid id' do
167
+ it 'returns true' do
168
+ VCR.use_cassette('organization/delete') do
169
+ response = described_class.delete_organization(
170
+ id: 'org_01F4A8TD0B4N1Y9SJ8SH635HDB',
171
+ )
172
+
173
+ expect(response).to be(true)
174
+ end
175
+ end
176
+ end
177
+
178
+ context 'with an invalid id' do
179
+ it 'returns false' do
180
+ VCR.use_cassette('organization/delete_invalid') do
181
+ expect do
182
+ described_class.delete_organization(id: 'invalid')
183
+ end.to raise_error(
184
+ WorkOS::APIError,
185
+ 'Status 404, Not Found - request ID: ',
186
+ )
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end