diff --git a/lib/workos.rb b/lib/workos.rb index f492f4c2..ae959d5c 100644 --- a/lib/workos.rb +++ b/lib/workos.rb @@ -81,6 +81,7 @@ def self.key autoload :VerifyChallenge, 'workos/verify_challenge' autoload :Webhook, 'workos/webhook' autoload :Webhooks, 'workos/webhooks' + autoload :Widgets, 'workos/widgets' # Errors autoload :APIError, 'workos/errors' diff --git a/lib/workos/types.rb b/lib/workos/types.rb index e912eaac..6b8cef87 100644 --- a/lib/workos/types.rb +++ b/lib/workos/types.rb @@ -7,5 +7,6 @@ module Types autoload :Intent, 'workos/types/intent' autoload :ListStruct, 'workos/types/list_struct' autoload :PasswordlessSessionStruct, 'workos/types/passwordless_session_struct' + autoload :WidgetScope, 'workos/types/widget_scope' end end diff --git a/lib/workos/types/widget_scope.rb b/lib/workos/types/widget_scope.rb new file mode 100644 index 00000000..d3ba05a5 --- /dev/null +++ b/lib/workos/types/widget_scope.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module WorkOS + module Types + # The WidgetScope constants are declarations of a fixed set of values for + # scopes while generating a widget token. + module WidgetScope + USERS_TABLE_MANAGE = 'widgets:users-table:manage' + + ALL = [USERS_TABLE_MANAGE].freeze + end + end +end diff --git a/lib/workos/widgets.rb b/lib/workos/widgets.rb new file mode 100644 index 00000000..bd1e7bd5 --- /dev/null +++ b/lib/workos/widgets.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'net/http' + +module WorkOS + # The Widgets module provides resource methods for working with the Widgets APIs + module Widgets + class << self + include Client + + WIDGET_SCOPES = WorkOS::Types::WidgetScope::ALL + + # Generate a widget token. + # + # @param [String] organization_id The ID of the organization to generate the token for. + # @param [String] user_id The ID of the user to generate the token for. + # @param [WidgetScope[]] The scopes to generate the token for. + def get_token(organization_id:, user_id:, scopes:) + validate_scopes(scopes) + + request = post_request( + auth: true, + body: { + organization_id: organization_id, + user_id: user_id, + scopes: scopes, + }, + path: '/widgets/token', + ) + + response = execute_request(request: request) + + JSON.parse(response.body)['token'] + end + + private + + def validate_scopes(scopes) + return if scopes.all? { |scope| WIDGET_SCOPES.include?(scope) } + + raise ArgumentError, 'scopes contains an invalid value.' \ + " Every item in `scopes` must be in #{WIDGET_SCOPES}" + end + end + end +end diff --git a/spec/lib/workos/widgets_spec.rb b/spec/lib/workos/widgets_spec.rb new file mode 100644 index 00000000..80bb328d --- /dev/null +++ b/spec/lib/workos/widgets_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +describe WorkOS::Widgets do + it_behaves_like 'client' + + describe '.get_token' do + let(:organization_id) { 'org_01JCP9G67MNAH0KC4B72XZ67M7' } + let(:user_id) { 'user_01JCP9H4SHS4N3J6XTKDT7JNPE' } + + describe 'with a valid organization_id and user_id and scopes' do + it 'returns a widget token' do + VCR.use_cassette 'widgets/get_token' do + token = described_class.get_token( + organization_id: organization_id, + user_id: user_id, + scopes: ['widgets:users-table:manage'], + ) + + expect(token).to start_with('eyJhbGciOiJSUzI1NiIsImtpZ') + end + end + end + + describe 'with an invalid organization_id' do + it 'raises an error' do + VCR.use_cassette 'widgets/get_token_invalid_organization_id' do + expect do + described_class.get_token( + organization_id: 'bogus-id', + user_id: user_id, + scopes: ['widgets:users-table:manage'], + ) + end.to raise_error( + WorkOS::NotFoundError, + /Organization not found: 'bogus-id'/, + ) + end + end + end + + describe 'with an invalid user_id' do + it 'raises an error' do + VCR.use_cassette 'widgets/get_token_invalid_user_id' do + expect do + described_class.get_token( + organization_id: organization_id, + user_id: 'bogus-id', + scopes: ['widgets:users-table:manage'], + ) + end.to raise_error( + WorkOS::NotFoundError, + /User not found: 'bogus-id'/, + ) + end + end + end + + describe 'with invalid scopes' do + it 'raises an error' do + expect do + described_class.get_token( + organization_id: organization_id, + user_id: user_id, + scopes: ['bogus-scope'], + ) + end.to raise_error( + ArgumentError, + /scopes contains an invalid value/, + ) + end + end + end +end diff --git a/spec/support/fixtures/vcr_cassettes/widgets/get_token.yml b/spec/support/fixtures/vcr_cassettes/widgets/get_token.yml new file mode 100644 index 00000000..1c9c42f6 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/widgets/get_token.yml @@ -0,0 +1,82 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.workos.com/widgets/token + body: + encoding: UTF-8 + string: '{"organization_id":"org_01JCP9G67MNAH0KC4B72XZ67M7","user_id":"user_01JCP9H4SHS4N3J6XTKDT7JNPE","scopes":["widgets:users-table:manage"]}' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - WorkOS; ruby/3.3.6; arm64-darwin23; v5.8.0 + Authorization: + - Bearer + response: + status: + code: 201 + message: Created + headers: + Date: + - Thu, 14 Nov 2024 21:51:34 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '791' + Connection: + - keep-alive + Cf-Ray: + - 8e2a394198f8c9b8-IAD + Cf-Cache-Status: + - DYNAMIC + Etag: + - W/"317-Nylo8f8lWbsA0UUWqqV59mFy5jo" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Vary: + - Origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'true' + Content-Security-Policy: + - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self'' + https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src + ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests' + Expect-Ct: + - max-age=0 + Referrer-Policy: + - no-referrer + X-Content-Type-Options: + - nosniff + X-Dns-Prefetch-Control: + - 'off' + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - bf98d35e-d9ca-437f-b937-150e937af0f1 + X-Xss-Protection: + - '0' + Set-Cookie: + - __cf_bm=GsR9Veicl9ZRIR1pUSamJ5m95HmklSbWNwtyp_fSpB4-1731621094-1.0.1.1-VW09qjPlT4T.AGwnsHxe7p_A.Onr9Oe7YnxumCz7B9XmzqYbLz9fx7cF6Qtw3KW0PIshpAVkluIsGWSCJQ5AjQ; + path=/; expires=Thu, 14-Nov-24 22:21:34 GMT; domain=.workos.com; HttpOnly; + Secure; SameSite=None + - __cfruid=022c638e9216cb6be687ace27cb356d48cbd4256-1731621094; path=/; domain=.workos.com; + HttpOnly; Secure; SameSite=None + - _cfuvid=kczJ.JXlRroyPs5B7UjNUynSmsUjYTWP_jcLNj2iiuM-1731621094755-0.0.1.1-604800000; + path=/; domain=.workos.com; HttpOnly; Secure; SameSite=None + Server: + - cloudflare + body: + encoding: UTF-8 + string: '{"token":"eyJhbGciOiJSUzI1NiIsImtpZCI6InNzb19vaWRjX2tleV9wYWlyXzAxSFY3SlpGWEtQOVhCQjc2NjY0TkdUQlpYIn0.eyJhdWQiOiJodHRwczovL2FwaS53b3Jrb3MuY29tIiwiaXNzIjoiaHR0cHM6Ly9hcGkud29ya29zLmNvbSIsInN1YiI6InVzZXJfMDFKQ1A5SDRTSFM0TjNKNlhUS0RUN0pOUEUiLCJqdGkiOiIwMUpDUEFKMUFHWDVESzFNM0hDQTk5MFM1SiIsIm9yZ19pZCI6Im9yZ18wMUpDUDlHNjdNTkFIMEtDNEI3MlhaNjdNNyIsInBlcm1pc3Npb25zIjpbInVzZXJzOm1hbmFnZSIsInVzZXJzOnZpZXciXSwiZXhwIjoxNzMxNjI0Njk0LCJpYXQiOjE3MzE2MjEwOTR9.CTYliFAGFjw-_Lyla-yVBOUAn1ZqU-J7aOdWhAW8fiEsNMz73Fb5nRACa0PFWBE3HK1a8waV-S5lBCGHyxgYOaew5URNnlYXVwlgpKwujHDrW47FrYpxkyxVovY9z9SqDDNRHWBqJM3mH_4Fn9jaHwAVT0SPJrJ7Q4-jxfTc0_sZMR7RVJaBIXPEU8og6Zwc84Gx-9A-mBUA3PPUXfaa8JrCr5OGc482vbD1rF5sjk0jx_FovHrlI3qRo5nkQ3_5WEi7LzdxSPviITxY1-dtm0HbeULz8IL7Ic5O4Ok4lB2c8s8XoZT1JqUMmEHfugkWyQ4juN5aHpmf6ux8cJSJWg"}' + http_version: + recorded_at: Thu, 14 Nov 2024 21:51:34 GMT +recorded_with: VCR 5.0.0 diff --git a/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_organization_id.yml b/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_organization_id.yml new file mode 100644 index 00000000..83721e1b --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_organization_id.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.workos.com/widgets/token + body: + encoding: UTF-8 + string: '{"organization_id":"bogus-id","user_id":"user_01JCP9H4SHS4N3J6XTKDT7JNPE","scopes":["widgets:users-table:manage"]}' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - WorkOS; ruby/3.3.6; arm64-darwin23; v5.8.0 + Authorization: + - Bearer + response: + status: + code: 404 + message: Not Found + headers: + Date: + - Thu, 14 Nov 2024 22:02:40 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 8e2a49858b5a7fa2-IAD + Cf-Cache-Status: + - DYNAMIC + Etag: + - W/"62-XNhANyOqo4doKt47ORHxpVuFTYg" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Vary: + - Origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'true' + Content-Security-Policy: + - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self'' + https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src + ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests' + Expect-Ct: + - max-age=0 + Referrer-Policy: + - no-referrer + X-Content-Type-Options: + - nosniff + X-Dns-Prefetch-Control: + - 'off' + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 3d383216-51fe-42cd-87e2-7fee32719353 + X-Xss-Protection: + - '0' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"message":"Organization not found: ''bogus-id''.","code":"entity_not_found","entity_id":"bogus-id"}' + http_version: + recorded_at: Thu, 14 Nov 2024 22:02:40 GMT +recorded_with: VCR 5.0.0 diff --git a/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_user_id.yml b/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_user_id.yml new file mode 100644 index 00000000..8ca40c91 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/widgets/get_token_invalid_user_id.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.workos.com/widgets/token + body: + encoding: UTF-8 + string: '{"organization_id":"org_01JCP9G67MNAH0KC4B72XZ67M7","user_id":"bogus-id","scopes":["widgets:users-table:manage"]}' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - WorkOS; ruby/3.3.6; arm64-darwin23; v5.8.0 + Authorization: + - Bearer + response: + status: + code: 404 + message: Not Found + headers: + Date: + - Thu, 14 Nov 2024 22:02:46 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - 8e2a49a82b31c54f-IAD + Cf-Cache-Status: + - DYNAMIC + Etag: + - W/"5a-TOigA+IvFyAtHvUdIXFXZWRdn8I" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Vary: + - Origin, Accept-Encoding + Access-Control-Allow-Credentials: + - 'true' + Content-Security-Policy: + - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self'' + https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src + ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests' + Expect-Ct: + - max-age=0 + Referrer-Policy: + - no-referrer + X-Content-Type-Options: + - nosniff + X-Dns-Prefetch-Control: + - 'off' + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 0aeb3b90-0fd7-4de9-8d76-3d0e340ed583 + X-Xss-Protection: + - '0' + Server: + - cloudflare + body: + encoding: ASCII-8BIT + string: '{"message":"User not found: ''bogus-id''.","code":"entity_not_found","entity_id":"bogus-id"}' + http_version: + recorded_at: Thu, 14 Nov 2024 22:02:46 GMT +recorded_with: VCR 5.0.0