diff --git a/.gitignore b/.gitignore index 03fb3387..28a094d7 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ # .rubocop-https?--* .vscode +.idea/ diff --git a/lib/workos/organization_membership.rb b/lib/workos/organization_membership.rb index daf04f09..4943b5ad 100644 --- a/lib/workos/organization_membership.rb +++ b/lib/workos/organization_membership.rb @@ -7,7 +7,7 @@ module WorkOS class OrganizationMembership include HashProvider - attr_accessor :id, :user_id, :organization_id, :status, :role, :created_at, :updated_at + attr_accessor :id, :user_id, :organization_id, :status, :role, :roles, :created_at, :updated_at def initialize(json) hash = JSON.parse(json, symbolize_names: true) @@ -17,6 +17,7 @@ def initialize(json) @organization_id = hash[:organization_id] @status = hash[:status] @role = hash[:role] + @roles = hash[:roles] @created_at = hash[:created_at] @updated_at = hash[:updated_at] end @@ -28,6 +29,7 @@ def to_json(*) organization_id: organization_id, status: status, role: role, + roles: roles, created_at: created_at, updated_at: updated_at, } diff --git a/lib/workos/session.rb b/lib/workos/session.rb index 44ff587c..a872487e 100644 --- a/lib/workos/session.rb +++ b/lib/workos/session.rb @@ -30,6 +30,7 @@ def initialize(user_management:, client_id:, session_data:, cookie_password:) # Authenticates the user based on the session data # @return [Hash] A hash containing the authentication response and a reason if the authentication failed + # rubocop:disable Metrics/AbcSize def authenticate return { authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' } if @session_data.nil? @@ -49,6 +50,7 @@ def authenticate session_id: decoded['sid'], organization_id: decoded['org_id'], role: decoded['role'], + roles: decoded['roles'], permissions: decoded['permissions'], entitlements: decoded['entitlements'], feature_flags: decoded['feature_flags'], @@ -64,7 +66,6 @@ def authenticate # @option options [String] :organization_id The organization ID to use for refreshing the session # @return [Hash] A hash containing a new sealed session, the authentication response, # and a reason if the refresh failed - # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/PerceivedComplexity def refresh(options = nil) cookie_password = options.nil? || options[:cookie_password].nil? ? @cookie_password : options[:cookie_password] diff --git a/lib/workos/user_management.rb b/lib/workos/user_management.rb index 8084e0e1..d0c8061a 100644 --- a/lib/workos/user_management.rb +++ b/lib/workos/user_management.rb @@ -926,16 +926,23 @@ def list_organization_memberships(options = {}) # @param [String] user_id The ID of the User. # @param [String] organization_id The ID of the Organization to which the user belongs to. # @param [String] role_slug The slug of the role to grant to this membership. (Optional) + # @param [Array] role_slugs Array of role slugs to assign to this membership. (Optional) # # @return [WorkOS::OrganizationMembership] - def create_organization_membership(user_id:, organization_id:, role_slug: nil) + def create_organization_membership(user_id:, organization_id:, role_slug: nil, role_slugs: nil) + raise ArgumentError, 'Cannot specify both role_slug and role_slugs' if role_slug && role_slugs + + body = { + user_id: user_id, + organization_id: organization_id, + } + + body[:role_slugs] = role_slugs if role_slugs + body[:role_slug] = role_slug if role_slug + request = post_request( path: '/user_management/organization_memberships', - body: { - user_id: user_id, - organization_id: organization_id, - role_slug: role_slug, - }.compact, + body: body.compact, auth: true, ) @@ -946,17 +953,22 @@ def create_organization_membership(user_id:, organization_id:, role_slug: nil) # Update an Organization Membership # - # @param [String] organization_membership_id The ID of the Organization Membership. - # @param [String] role_slug The slug of the role to grant to this membership. + # @param [String] id The ID of the Organization Membership. + # @param [String] role_slug The slug of the role to grant to this membership. (Optional) + # @param [Array] role_slugs Array of role slugs to assign to this membership. (Optional) # # @return [WorkOS::OrganizationMembership] - def update_organization_membership(id:, role_slug:) + def update_organization_membership(id:, role_slug: nil, role_slugs: nil) + raise ArgumentError, 'Cannot specify both role_slug and role_slugs' if role_slug && role_slugs + + body = { id: id } + + body[:role_slugs] = role_slugs if role_slugs + body[:role_slug] = role_slug if role_slug + request = put_request( path: "/user_management/organization_memberships/#{id}", - body: { - id: id, - role_slug: role_slug, - }, + body: body.compact, auth: true, ) diff --git a/spec/lib/workos/session_spec.rb b/spec/lib/workos/session_spec.rb index 6050bcea..d0075650 100644 --- a/spec/lib/workos/session_spec.rb +++ b/spec/lib/workos/session_spec.rb @@ -108,6 +108,7 @@ sid: 'session_id', org_id: 'org_id', role: 'role', + roles: ['role'], permissions: ['read'], exp: Time.now.to_i + 3600, } @@ -173,6 +174,7 @@ session_id: 'session_id', organization_id: 'org_id', role: 'role', + roles: ['role'], permissions: ['read'], feature_flags: nil, entitlements: nil, @@ -188,6 +190,7 @@ sid: 'session_id', org_id: 'org_id', role: 'role', + roles: ['role'], permissions: ['read'], entitlements: ['billing'], exp: Time.now.to_i + 3600, @@ -208,6 +211,7 @@ session_id: 'session_id', organization_id: 'org_id', role: 'role', + roles: ['role'], permissions: ['read'], entitlements: ['billing'], feature_flags: nil, @@ -224,6 +228,7 @@ sid: 'session_id', org_id: 'org_id', role: 'role', + roles: ['role'], permissions: ['read'], feature_flags: ['new_feature_enabled'], exp: Time.now.to_i + 3600, @@ -244,6 +249,7 @@ session_id: 'session_id', organization_id: 'org_id', role: 'role', + roles: ['role'], permissions: ['read'], entitlements: nil, feature_flags: ['new_feature_enabled'], diff --git a/spec/lib/workos/user_management_spec.rb b/spec/lib/workos/user_management_spec.rb index 1192e936..d137a795 100644 --- a/spec/lib/workos/user_management_spec.rb +++ b/spec/lib/workos/user_management_spec.rb @@ -1302,6 +1302,23 @@ end end end + + context 'with role slugs' do + it 'creates an organization membership with multiple roles' do + VCR.use_cassette 'user_management/create_organization_membership/valid_multiple_roles' do + organization_membership = described_class.create_organization_membership( + user_id: 'user_01H5JQDV7R7ATEYZDEG0W5PRYS', + organization_id: 'org_01H5JQDV7R7ATEYZDEG0W5PRYS', + role_slugs: %w[admin member], + ) + + expect(organization_membership.organization_id).to eq('organization_01H5JQDV7R7ATEYZDEG0W5PRYS') + expect(organization_membership.user_id).to eq('user_01H5JQDV7R7ATEYZDEG0W5PRYS') + expect(organization_membership.roles).to be_an(Array) + expect(organization_membership.roles.length).to eq(2) + end + end + end end describe '.update_organization_membership' do @@ -1329,6 +1346,22 @@ end end end + + context 'with role slugs' do + it 'updates an organization membership with multiple roles' do + VCR.use_cassette('user_management/update_organization_membership/valid_multiple_roles') do + organization_membership = WorkOS::UserManagement.update_organization_membership( + id: 'om_01H5JQDV7R7ATEYZDEG0W5PRYS', + role_slugs: %w[admin editor], + ) + + expect(organization_membership.organization_id).to eq('organization_01H5JQDV7R7ATEYZDEG0W5PRYS') + expect(organization_membership.user_id).to eq('user_01H5JQDV7R7ATEYZDEG0W5PRYS') + expect(organization_membership.roles).to be_an(Array) + expect(organization_membership.roles.length).to eq(2) + end + end + end end describe '.delete_organization_membership' do diff --git a/spec/support/fixtures/vcr_cassettes/user_management/create_organization_membership/valid_multiple_roles.yml b/spec/support/fixtures/vcr_cassettes/user_management/create_organization_membership/valid_multiple_roles.yml new file mode 100644 index 00000000..13afb89f --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/user_management/create_organization_membership/valid_multiple_roles.yml @@ -0,0 +1,76 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.workos.com/user_management/organization_memberships + body: + encoding: UTF-8 + string: '{"user_id":"user_01H5JQDV7R7ATEYZDEG0W5PRYS","organization_id":"org_01H5JQDV7R7ATEYZDEG0W5PRYS","role_slugs":["admin","member"]}' + 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.1.4; arm64-darwin24; v5.25.0 + Authorization: + - Bearer + response: + status: + code: 201 + message: Created + headers: + Date: + - Mon, 06 Oct 2025 19:57:34 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '26' + Connection: + - keep-alive + Server: + - cloudflare + Cf-Ray: + - 98a7ba7fea91ead7-ORD + Cf-Cache-Status: + - DYNAMIC + Etag: + - W/"1a-pljHtlo127JYJR4E/RYOPb6ucbw" + 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: + - 6507807d-d3b9-4e8f-9ca8-f84c6f9c6eb4 + X-Xss-Protection: + - '0' + Set-Cookie: + - _cfuvid=recfIuOTSO3jIxK5SUOdhHRRfTGoiOFOMgSbEpVIjpg-1759780654123-0.0.1.1-604800000; + path=/; domain=.workos.com; HttpOnly; Secure; SameSite=None + body: + encoding: UTF-8 + string: '{"object":"organization_membership","id":"om_01H5JQDV7R7ATEYZDEG0W5PRYS","user_id":"user_01H5JQDV7R7ATEYZDEG0W5PRYS","organization_id":"organization_01H5JQDV7R7ATEYZDEG0W5PRYS","status":"active","role":{"slug":"member"}, "roles":[{"slug":"member"}, {"slug":"admin"}], "created_at":"2023-07-18T02:07:19.911Z","updated_at":"2023-07-18T02:07:19.911Z"}' + recorded_at: Mon, 06 Oct 2025 19:57:34 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/support/fixtures/vcr_cassettes/user_management/update_organization_membership/valid_multiple_roles.yml b/spec/support/fixtures/vcr_cassettes/user_management/update_organization_membership/valid_multiple_roles.yml new file mode 100644 index 00000000..7cb2dd46 --- /dev/null +++ b/spec/support/fixtures/vcr_cassettes/user_management/update_organization_membership/valid_multiple_roles.yml @@ -0,0 +1,76 @@ +--- +http_interactions: +- request: + method: put + uri: https://api.workos.com/user_management/organization_memberships/om_01H5JQDV7R7ATEYZDEG0W5PRYS + body: + encoding: UTF-8 + string: '{"id":"om_01H5JQDV7R7ATEYZDEG0W5PRYS","role_slugs":["admin","editor"]}' + 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.1.4; arm64-darwin24; v5.25.0 + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 07 Oct 2025 14:22:26 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '26' + Connection: + - keep-alive + Server: + - cloudflare + Cf-Ray: + - 98ae0cf66dd9344d-ORD + Cf-Cache-Status: + - DYNAMIC + Etag: + - W/"1a-pljHtlo127JYJR4E/RYOPb6ucbw" + 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: + - 92b6245b-40a8-4bdd-95d1-e32f6e9d1d70 + X-Xss-Protection: + - '0' + Set-Cookie: + - _cfuvid=hq7zKar8D7SHPwNzTjLw.29beujgLQlq6cOYaWvYhEM-1759846946364-0.0.1.1-604800000; + path=/; domain=.workos.com; HttpOnly; Secure; SameSite=None + body: + encoding: UTF-8 + string: '{"object":"organization_membership","id":"om_01H5JQDV7R7ATEYZDEG0W5PRYS","user_id":"user_01H5JQDV7R7ATEYZDEG0W5PRYS","organization_id":"organization_01H5JQDV7R7ATEYZDEG0W5PRYS","status":"active","role":{"slug":"admin"},"roles":[{"slug":"admin"},{"slug":"editor"}],"created_at":"2023-07-18T02:07:19.911Z","updated_at":"2023-07-18T02:07:19.911Z"}' + recorded_at: Tue, 07 Oct 2025 14:22:26 GMT +recorded_with: VCR 6.3.1