From b249fd36b4ebf2ff9f06a6048944beabbe5e1c6c Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Thu, 10 Jul 2025 18:00:19 +0200 Subject: [PATCH] [#65747] Make /scim_v2/Schemas endpoint return required by OP schemas. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://community.openproject.org/work_packages/65747 - Make /scim_v2/Schemas endpoint return OP required schemas. Set required field to be required(givenName, familyName, email etc.). Set caseSensetive to be true where it is case sensetive. Remove fields that are not used by OpenProject. Note: making fields as requried make scimitar check their presence and responds with 400 which is useful. - Refactor SCIM API controllers by extracting error handling in a general(🫡) method - Add extra specs - Restyle SCIM API specs a bit with hope that it makes them more readable... --- .../scim_v2/base_controller_actions.rb | 21 + app/controllers/scim_v2/groups_controller.rb | 50 +-- app/controllers/scim_v2/users_controller.rb | 50 +-- config/initializers/scimitar.rb | 4 +- lib/scimitar_schema_extension.rb | 44 ++- spec/requests/scim_v2/groups_spec.rb | 308 +++++++-------- spec/requests/scim_v2/schemas_spec.rb | 94 ++++- spec/requests/scim_v2/users_spec.rb | 359 ++++++++++-------- 8 files changed, 546 insertions(+), 384 deletions(-) diff --git a/app/controllers/scim_v2/base_controller_actions.rb b/app/controllers/scim_v2/base_controller_actions.rb index b1778c0ce07..e343384665b 100644 --- a/app/controllers/scim_v2/base_controller_actions.rb +++ b/app/controllers/scim_v2/base_controller_actions.rb @@ -89,6 +89,27 @@ module ScimV2 all_possible_attributes - excluded_attributes - excluded_parents end + + def raise_result_errors_for_scim(result) + result.on_failure do |result| + uniqueness_error = result.errors.find { |e| e.type == :taken } + unauthorized_error = result.errors.find { |e| e.type == :error_unauthorized } + if uniqueness_error.present? + raise Scimitar::ErrorResponse.new( + status: 409, + scimType: "uniqueness", + detail: "Operation failed due to a uniqueness constraint: #{result.message}" + ) + elsif unauthorized_error.present? + raise Scimitar::ErrorResponse.new( + status: 403, + detail: "Action forbidden: insufficient permissions." + ) + else + raise result.message + end + end + end end end end diff --git a/app/controllers/scim_v2/groups_controller.rb b/app/controllers/scim_v2/groups_controller.rb index 78389fddd36..685a10cb928 100644 --- a/app/controllers/scim_v2/groups_controller.rb +++ b/app/controllers/scim_v2/groups_controller.rb @@ -40,26 +40,15 @@ module ScimV2 call = Groups::CreateService .new(user: User.current, model: group) .call(group.attributes) - .on_failure do |result| - uniqueness_error = result.errors.find { |e| e.type == :taken } - if uniqueness_error.present? - raise Scimitar::ErrorResponse.new( - status: 409, - scimType: "uniqueness", - detail: "Operation failed due to a uniqueness constraint: #{result.message}" - ) - else - raise result.message - end - end + raise_result_errors_for_scim(call) group = call.result - members = scim_resource.members if members.present? - Groups::AddUsersService - .new(group, current_user: User.system) - .call(ids: members.map(&:value), send_notifications: false) - .on_failure { |call| raise call.message } + raise_result_errors_for_scim( + Groups::AddUsersService + .new(group, current_user: User.system) + .call(ids: members.map(&:value), send_notifications: false) + ) end group.to_scim( @@ -75,10 +64,11 @@ module ScimV2 storage_class.transaction do group = storage_scope.find(group_id) group.from_scim!(scim_hash: scim_resource.as_json) - Groups::UpdateService - .new(user: User.current, model: group) - .call(user_ids: scim_resource.members.map(&:value)) - .on_failure { |call| raise call.message } + raise_result_errors_for_scim( + Groups::UpdateService + .new(user: User.current, model: group) + .call(user_ids: scim_resource.members.map(&:value)) + ) group.reload group.to_scim( location: url_for(action: :show, id: group.id), @@ -94,10 +84,11 @@ module ScimV2 group = storage_scope.find(group_id) group.from_scim_patch!(patch_hash: patch_hash) user_ids = group.scim_members.map(&:id) - Groups::UpdateService - .new(user: User.current, model: group) - .call(user_ids:) - .on_failure { |call| raise call.message } + raise_result_errors_for_scim( + Groups::UpdateService + .new(user: User.current, model: group) + .call(user_ids:) + ) group.reload group.to_scim( location: url_for(action: :show, id: group.id), @@ -110,10 +101,11 @@ module ScimV2 def destroy super do |group_id| group = storage_scope.find(group_id) - Groups::DeleteService - .new(user: User.current, model: group) - .call - .on_failure { |call| raise call.message } + raise_result_errors_for_scim( + Groups::DeleteService + .new(user: User.current, model: group) + .call + ) end end diff --git a/app/controllers/scim_v2/users_controller.rb b/app/controllers/scim_v2/users_controller.rb index aaeb30ebb59..90eaa881760 100644 --- a/app/controllers/scim_v2/users_controller.rb +++ b/app/controllers/scim_v2/users_controller.rb @@ -40,19 +40,7 @@ module ScimV2 call = Users::CreateService .new(user: User.current, model: user) .call(user.attributes) - .on_failure do |result| - uniqueness_error = result.errors.find { |e| e.type == :taken } - if uniqueness_error.present? - raise Scimitar::ErrorResponse.new( - status: 409, - scimType: "uniqueness", - detail: "Operation failed due to a uniqueness constraint: #{result.message}" - ) - else - raise result.message - end - end - + raise_result_errors_for_scim(call) user = call.result user.to_scim( location: url_for(action: :show, id: user.id), @@ -67,10 +55,11 @@ module ScimV2 storage_class.transaction do user = storage_scope.find(user_id) user.from_scim!(scim_hash: scim_resource.as_json) - Users::UpdateService - .new(user: User.current, model: user) - .call - .on_failure { |call| raise call.message } + call = Users::UpdateService + .new(user: User.current, model: user) + .call + raise_result_errors_for_scim(call) + user = call.result user.to_scim( location: url_for(action: :show, id: user.id), include_attributes: @@ -84,10 +73,11 @@ module ScimV2 storage_class.transaction do user = storage_scope.find(user_id) user.from_scim_patch!(patch_hash: patch_hash) - Users::UpdateService - .new(user: User.current, model: user) - .call - .on_failure { |call| raise call.message } + call = Users::UpdateService + .new(user: User.current, model: user) + .call + raise_result_errors_for_scim(call) + user = call.result user.to_scim( location: url_for(action: :show, id: user.id), include_attributes: @@ -99,20 +89,10 @@ module ScimV2 def destroy super do |user_id| user = storage_scope.find(user_id) - Users::DeleteService - .new(user: User.current, model: user) - .call - .on_failure do |result| - unauthorized_error = result.errors.find { |e| e.type == :error_unauthorized } - if unauthorized_error.present? - raise Scimitar::ErrorResponse.new( - status: 403, - detail: "User can't be deleted due to permission absence." - ) - else - raise result.message - end - end + call = Users::DeleteService + .new(user: User.current, model: user) + .call + raise_result_errors_for_scim(call) end end diff --git a/config/initializers/scimitar.rb b/config/initializers/scimitar.rb index 266285e1c8b..5a03a0d9e0f 100644 --- a/config/initializers/scimitar.rb +++ b/config/initializers/scimitar.rb @@ -38,10 +38,10 @@ Rails.application.config.to_prepare do ) Scimitar::Schema::User.singleton_class.class_eval do - prepend ScimitarSchemaExtension + prepend ScimitarSchemaExtension::User end Scimitar::Schema::Group.singleton_class.class_eval do - prepend ScimitarSchemaExtension + prepend ScimitarSchemaExtension::Group end end diff --git a/lib/scimitar_schema_extension.rb b/lib/scimitar_schema_extension.rb index c94019ab766..66620f142a1 100644 --- a/lib/scimitar_schema_extension.rb +++ b/lib/scimitar_schema_extension.rb @@ -28,11 +28,43 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ScimitarSchemaExtension - def scim_attributes - super + [Scimitar::Schema::Attribute.new(name: "externalId", - type: "string", - caseExact: true, - required: true)] +class OpenProjectNameSchema < Scimitar::Schema::Base + def self.scim_attributes + @scim_attributes ||= [ + Scimitar::Schema::Attribute.new(name: "familyName", caseExact: true, type: "string", required: true), + Scimitar::Schema::Attribute.new(name: "givenName", caseExact: true, type: "string", required: true) + ] + end +end + +class OpenProjectNameComplexType < Scimitar::ComplexTypes::Base + set_schema OpenProjectNameSchema +end + +module ScimitarSchemaExtension + module Group + def scim_attributes + [ + Scimitar::Schema::Attribute.new(name: "displayName", caseExact: true, type: "string", required: true), + Scimitar::Schema::Attribute.new(name: "members", multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceMember, + mutability: "readWrite"), + Scimitar::Schema::Attribute.new(name: "externalId", type: "string", caseExact: true, required: true) + ] + end + end + + module User + def scim_attributes + [ + Scimitar::Schema::Attribute.new(name: "userName", caseExact: true, type: "string", uniqueness: "server", required: true), + Scimitar::Schema::Attribute.new(name: "name", caseExact: true, complexType: OpenProjectNameComplexType, required: true), + Scimitar::Schema::Attribute.new(name: "active", type: "boolean"), + Scimitar::Schema::Attribute.new(name: "emails", multiValued: true, complexType: Scimitar::ComplexTypes::Email, + required: true), + Scimitar::Schema::Attribute.new(name: "groups", multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, + mutability: "readOnly"), + Scimitar::Schema::Attribute.new(name: "externalId", type: "string", caseExact: true, required: true) + ] + end end end diff --git a/spec/requests/scim_v2/groups_spec.rb b/spec/requests/scim_v2/groups_spec.rb index 88703e8f78d..14c8507fc18 100644 --- a/spec/requests/scim_v2/groups_spec.rb +++ b/spec/requests/scim_v2/groups_spec.rb @@ -61,26 +61,26 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do get "/scim_v2/Groups", {}, headers response_body = JSON.parse(last_response.body) - expect(response_body).to match({ "Resources" => contain_exactly({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [{ "value" => user.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }, { "displayName" => group_without_external_id.name, - "id" => group_without_external_id.id.to_s, - "members" => [{ "value" => user.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group_without_external_id.id}", - "created" => group_without_external_id.created_at.iso8601, - "lastModified" => group_without_external_id.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }), - "itemsPerPage" => 100, - "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], - "startIndex" => 1, - "totalResults" => 2 }) + expect(response_body).to match("Resources" => contain_exactly({ "displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [{ "value" => user.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }, { "displayName" => group_without_external_id.name, + "id" => group_without_external_id.id.to_s, + "members" => [{ "value" => user.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group_without_external_id.id}", + "created" => group_without_external_id.created_at.iso8601, + "lastModified" => group_without_external_id.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }), + "itemsPerPage" => 100, + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + "startIndex" => 1, + "totalResults" => 2) end it "filters results" do @@ -88,29 +88,29 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do get "/scim_v2/Groups?filter=#{filter}", {}, headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "Resources" => [{ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [{ "value" => user.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }], - "itemsPerPage" => 100, - "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], - "startIndex" => 1, - "totalResults" => 1 }) + expect(response_body).to eq("Resources" => [{ "displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [{ "value" => user.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }], + "itemsPerPage" => 100, + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + "startIndex" => 1, + "totalResults" => 1) filter = ERB::Util.url_encode('displayName Eq "NONEXISTENT GROUP NAME"') get "/scim_v2/Groups?filter=#{filter}", {}, headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "Resources" => [], - "itemsPerPage" => 100, - "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], - "startIndex" => 1, - "totalResults" => 0 }) + expect(response_body).to eq("Resources" => [], + "itemsPerPage" => 100, + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + "startIndex" => 1, + "totalResults" => 0) end end @@ -120,8 +120,9 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -135,29 +136,29 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do get "/scim_v2/Groups/#{group.id}", {}, headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [{ "value" => user.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [{ "value" => user.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end it "excludes specified attributes" do get "/scim_v2/Groups/#{group.id}?excludedAttributes=members", {}, headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) expect(response_body["members"]).to be_nil end end @@ -168,8 +169,9 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -192,15 +194,15 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) group = Group.find_by(name: group_name) - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [{ "value" => user.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [{ "value" => user.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end it "creates group without members specified" do @@ -214,15 +216,15 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) group = Group.find_by(name: group_name) - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end end @@ -232,8 +234,9 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -252,15 +255,15 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do get "/scim_v2/Groups/#{group.id}", "", headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [{ "value" => user.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [{ "value" => user.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) perform_enqueued_jobs assert_performed_jobs 1 @@ -269,9 +272,9 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Resource \"#{group.id}\" not found", - "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "404" } + "detail" => "Resource \"#{group.id}\" not found", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "404" ) end end @@ -282,8 +285,9 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -311,15 +315,15 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) group.reload - expect(response_body).to match({ "displayName" => group.name, - "externalId" => new_external_group_id, - "id" => group.id.to_s, - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "members" => contain_exactly({ "value" => user.id.to_s }, { "value" => admin.id.to_s }), - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to match("displayName" => group.name, + "externalId" => new_external_group_id, + "id" => group.id.to_s, + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "members" => contain_exactly({ "value" => user.id.to_s }, { "value" => admin.id.to_s }), + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end end @@ -329,8 +333,9 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -355,15 +360,15 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) group.reload - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => new_external_group_id, - "id" => group.id.to_s, - "members" => [{ "value" => user.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => new_external_group_id, + "id" => group.id.to_s, + "members" => [{ "value" => user.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end it "supports replacing of members" do @@ -385,15 +390,15 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) group.reload - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [{ "value" => user2.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [{ "value" => user2.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end it "supports adding of a member" do @@ -413,16 +418,16 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) group.reload - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [{ "value" => user1.id.to_s }, - { "value" => user2.id.to_s }], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [{ "value" => user1.id.to_s }, + { "value" => user2.id.to_s }], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end it "supports removal of a member" do @@ -441,15 +446,15 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) group.reload - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "members" => [], - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "members" => [], + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end it "supports removal of a member with exclusion of members list from the response" do @@ -468,14 +473,14 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) group.reload - expect(response_body).to eq({ "displayName" => group.name, - "externalId" => external_group_id, - "id" => group.id.to_s, - "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", - "created" => group.created_at.iso8601, - "lastModified" => group.updated_at.iso8601, - "resourceType" => "Group" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"] }) + expect(response_body).to eq("displayName" => group.name, + "externalId" => external_group_id, + "id" => group.id.to_s, + "meta" => { "location" => "http://test.host/scim_v2/Groups/#{group.id}", + "created" => group.created_at.iso8601, + "lastModified" => group.updated_at.iso8601, + "resourceType" => "Group" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:Group"]) end end @@ -485,8 +490,9 @@ RSpec.describe "SCIM API Groups", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) end end diff --git a/spec/requests/scim_v2/schemas_spec.rb b/spec/requests/scim_v2/schemas_spec.rb index f2b205176a6..ffd097fb85a 100644 --- a/spec/requests/scim_v2/schemas_spec.rb +++ b/spec/requests/scim_v2/schemas_spec.rb @@ -42,26 +42,93 @@ RSpec.describe "SCIM API Schemas", with_ee: [:scim_api] do describe "GET /scim_v2/Schemas" do context "with the feature flag enabled", with_flag: { scim_api: true } do + # rubocop:disable Layout/LineLength it "responds with supported schemas" do get "/scim_v2/Schemas", {}, headers response_body = JSON.parse(last_response.body) expect(response_body["totalResults"]).to eq(2) expect(response_body["schemas"]).to eq(["urn:ietf:params:scim:api:messages:2.0:ListResponse"]) - expect(response_body["schemas"]).to eq(["urn:ietf:params:scim:api:messages:2.0:ListResponse"]) group_schema = response_body["Resources"].find { |r| r["name"] == "Group" } user_schema = response_body["Resources"].find { |r| r["name"] == "User" } - external_id_schema = { "multiValued" => false, - "required" => true, - "caseExact" => true, - "mutability" => "readWrite", - "uniqueness" => "none", - "returned" => "default", - "name" => "externalId", - "type" => "string" } - expect(group_schema["attributes"]).to include(external_id_schema) - expect(user_schema["attributes"]).to include(external_id_schema) + + expect(group_schema).to eq( + "name" => "Group", + "id" => "urn:ietf:params:scim:schemas:core:2.0:Group", + "description" => "Represents a Group", + "meta" => { "resourceType" => "Schema", + "location" => "http://test.host/scim_v2/Schemas?name=urn%3Aietf%3Aparams%3Ascim%3Aschemas%3Acore%3A2.0%3AGroup" }, + "attributes" => [ + { "multiValued" => false, "required" => true, "caseExact" => true, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "displayName", "type" => "string" }, + { "multiValued" => true, + "required" => false, + "caseExact" => false, + "mutability" => "readWrite", + "uniqueness" => "none", + "returned" => "default", + "type" => "complex", + "subAttributes" => [ + { "multiValued" => false, "required" => true, "caseExact" => false, "mutability" => "immutable", "uniqueness" => "none", "returned" => "default", "name" => "value", "type" => "string" }, + { "multiValued" => false, "required" => false, "caseExact" => false, "mutability" => "immutable", "uniqueness" => "none", "returned" => "default", "name" => "type", "type" => "string" }, + { "multiValued" => false, "required" => false, "caseExact" => false, "mutability" => "immutable", "uniqueness" => "none", "returned" => "default", "name" => "display", "type" => "string" } + ], + "name" => "members" }, + { "multiValued" => false, "required" => true, "caseExact" => true, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "externalId", "type" => "string" } + ] + ) + expect(user_schema).to eq( + "name" => "User", + "id" => "urn:ietf:params:scim:schemas:core:2.0:User", + "description" => "Represents a User", + "meta" => { "resourceType" => "Schema", + "location" => "http://test.host/scim_v2/Schemas?name=urn%3Aietf%3Aparams%3Ascim%3Aschemas%3Acore%3A2.0%3AUser" }, + "attributes" => [ + { "multiValued" => false, "required" => true, "caseExact" => true, "mutability" => "readWrite", "uniqueness" => "server", "returned" => "default", "name" => "userName", "type" => "string" }, + { "multiValued" => false, + "required" => true, + "caseExact" => true, + "mutability" => "readWrite", + "uniqueness" => "none", + "returned" => "default", + "type" => "complex", + "subAttributes" => [ + { "multiValued" => false, "required" => true, "caseExact" => true, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "familyName", "type" => "string" }, + { "multiValued" => false, "required" => true, "caseExact" => true, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "givenName", "type" => "string" } + ], + "name" => "name" }, + { "multiValued" => false, "required" => false, "caseExact" => false, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "active", "type" => "boolean" }, + { "multiValued" => true, + "required" => true, + "caseExact" => false, + "mutability" => "readWrite", + "uniqueness" => "none", + "returned" => "default", + "type" => "complex", + "subAttributes" => [ + { "multiValued" => false, "required" => true, "caseExact" => false, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "value", "type" => "string" }, + { "multiValued" => false, "required" => false, "caseExact" => false, "mutability" => "readOnly", "uniqueness" => "none", "returned" => "default", "name" => "display", "type" => "string" }, + { "multiValued" => false, "required" => false, "caseExact" => false, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "type", "type" => "string" }, + { "multiValued" => false, "required" => false, "caseExact" => false, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "primary", "type" => "boolean" } + ], + "name" => "emails" }, + { "multiValued" => true, + "required" => false, + "caseExact" => false, + "mutability" => "readOnly", + "uniqueness" => "none", + "returned" => "default", + "type" => "complex", + "subAttributes" => [ + { "multiValued" => false, "required" => true, "caseExact" => false, "mutability" => "readOnly", "uniqueness" => "none", "returned" => "default", "name" => "value", "type" => "string" }, + { "multiValued" => false, "required" => false, "caseExact" => false, "mutability" => "readOnly", "uniqueness" => "none", "returned" => "default", "name" => "display", "type" => "string" }, + { "multiValued" => false, "required" => false, "caseExact" => false, "mutability" => "readOnly", "uniqueness" => "none", "returned" => "default", "name" => "type", "type" => "string" } + ], + "name" => "groups" }, + { "multiValued" => false, "required" => true, "caseExact" => true, "mutability" => "readWrite", "uniqueness" => "none", "returned" => "default", "name" => "externalId", "type" => "string" } + ] + ) end + # rubocop:enable Layout/LineLength end context "with the feature flag disabled", with_flag: { scim_api: false } do @@ -70,8 +137,9 @@ RSpec.describe "SCIM API Schemas", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) end end diff --git a/spec/requests/scim_v2/users_spec.rb b/spec/requests/scim_v2/users_spec.rb index 821ae06a175..12eec2793d3 100644 --- a/spec/requests/scim_v2/users_spec.rb +++ b/spec/requests/scim_v2/users_spec.rb @@ -122,11 +122,11 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do get "/scim_v2/Users?filter=#{filter_with_nonexisting_rows}", {}, headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "Resources" => [], - "itemsPerPage" => 100, - "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], - "startIndex" => 1, - "totalResults" => 0 }) + expect(response_body).to eq("Resources" => [], + "itemsPerPage" => 100, + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + "startIndex" => 1, + "totalResults" => 0) end it "filters results by externalId" do @@ -158,11 +158,11 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do get "/scim_v2/Users?filter=#{filter_with_nonexisting_rows}", {}, headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "Resources" => [], - "itemsPerPage" => 100, - "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], - "startIndex" => 1, - "totalResults" => 0 }) + expect(response_body).to eq("Resources" => [], + "itemsPerPage" => 100, + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + "startIndex" => 1, + "totalResults" => 0) end end @@ -172,8 +172,9 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -187,21 +188,21 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do get "/scim_v2/Users/#{user.id}", {}, headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "active" => true, - "emails" => [{ "primary" => true, - "type" => "work", - "value" => user.mail }], - "externalId" => external_user_id, - "groups" => [{ "value" => group.id.to_s }], - "id" => user.id.to_s, - "meta" => { "created" => user.created_at.iso8601, - "lastModified" => user.updated_at.iso8601, - "location" => "http://test.host/scim_v2/Users/#{user.id}", - "resourceType" => "User" }, - "name" => { "familyName" => user.lastname, - "givenName" => user.firstname }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], - "userName" => user.login }) + expect(response_body).to eq("active" => true, + "emails" => [{ "primary" => true, + "type" => "work", + "value" => user.mail }], + "externalId" => external_user_id, + "groups" => [{ "value" => group.id.to_s }], + "id" => user.id.to_s, + "meta" => { "created" => user.created_at.iso8601, + "lastModified" => user.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{user.id}", + "resourceType" => "User" }, + "name" => { "familyName" => user.lastname, + "givenName" => user.firstname }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => user.login) end end @@ -211,8 +212,9 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -247,14 +249,71 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do expect(last_response).to have_http_status(409) response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "detail" => "Operation failed due to a uniqueness constraint: Username has already been taken.", - "status" => "409", - "scimType" => "uniqueness" } + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "detail" => "Operation failed due to a uniqueness constraint: Username has already been taken.", + "status" => "409", + "scimType" => "uniqueness" ) end end + it "responds with 400 when email is missing" do + group + request_body = { + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "externalId" => external_user_id, + "userName" => user.login, + "name" => { + "givenName" => "John", + "familyName" => "Doe" + }, + "active" => true, + "emails" => [] + } + + post "/scim_v2/Users/", request_body.to_json, headers + + expect(last_response).to have_http_status(400) + response_body = JSON.parse(last_response.body) + expect(response_body).to eq( + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "detail" => "Invalid resource: Emails is required.", + "status" => "400", + "scimType" => "invalidValue" + ) + end + + it "responds with 400 when familyName is missing" do + group + request_body = { + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "externalId" => external_user_id, + "userName" => user.login, + "name" => { + "givenName" => "John" + }, + "active" => true, + "emails" => [ + { + "value" => "jdoe@example.com", + "type" => "work", + "primary" => true + } + ] + } + + post "/scim_v2/Users/", request_body.to_json, headers + + expect(last_response).to have_http_status(400) + response_body = JSON.parse(last_response.body) + expect(response_body).to eq( + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "detail" => "Invalid resource: Name familyname is required.", + "status" => "400", + "scimType" => "invalidValue" + ) + end + it "creates user with provided data and excludes some attributes" do request_body = { "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], @@ -278,17 +337,17 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) created_user = User.find_by(login: "jdoe") expect(created_user).to be_present - expect(response_body).to eq({ "active" => true, - "externalId" => external_user_id, - "groups" => [], - "id" => created_user.id.to_s, - "meta" => { "created" => created_user.created_at.iso8601, - "lastModified" => created_user.updated_at.iso8601, - "location" => "http://test.host/scim_v2/Users/#{created_user.id}", - "resourceType" => "User" }, - "name" => { "familyName" => "Doe" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], - "userName" => "jdoe" }) + expect(response_body).to eq("active" => true, + "externalId" => external_user_id, + "groups" => [], + "id" => created_user.id.to_s, + "meta" => { "created" => created_user.created_at.iso8601, + "lastModified" => created_user.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{created_user.id}", + "resourceType" => "User" }, + "name" => { "familyName" => "Doe" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => "jdoe") end it "creates user with provided data" do @@ -314,21 +373,21 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) created_user = User.find_by(login: "jdoe") expect(created_user).to be_present - expect(response_body).to eq({ "active" => true, - "emails" => [{ "primary" => true, - "type" => "work", - "value" => "jdoe@example.com" }], - "externalId" => external_user_id, - "groups" => [], - "id" => created_user.id.to_s, - "meta" => { "created" => created_user.created_at.iso8601, - "lastModified" => created_user.updated_at.iso8601, - "location" => "http://test.host/scim_v2/Users/#{created_user.id}", - "resourceType" => "User" }, - "name" => { "familyName" => "Doe", - "givenName" => "John" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], - "userName" => "jdoe" }) + expect(response_body).to eq("active" => true, + "emails" => [{ "primary" => true, + "type" => "work", + "value" => "jdoe@example.com" }], + "externalId" => external_user_id, + "groups" => [], + "id" => created_user.id.to_s, + "meta" => { "created" => created_user.created_at.iso8601, + "lastModified" => created_user.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{created_user.id}", + "resourceType" => "User" }, + "name" => { "familyName" => "Doe", + "givenName" => "John" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => "jdoe") end it "creates user with any email type string provided" do @@ -352,21 +411,21 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) created_user = User.find_by(login: "jdoe") - expect(response_body).to eq({ "active" => true, - "emails" => [{ "primary" => true, - "type" => "work", - "value" => "jdoe@example.com" }], - "externalId" => external_user_id, - "groups" => [], - "id" => created_user.id.to_s, - "meta" => { "created" => created_user.created_at.iso8601, - "lastModified" => created_user.updated_at.iso8601, - "location" => "http://test.host/scim_v2/Users/#{created_user.id}", - "resourceType" => "User" }, - "name" => { "familyName" => "Doe", - "givenName" => "John" }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], - "userName" => "jdoe" }) + expect(response_body).to eq("active" => true, + "emails" => [{ "primary" => true, + "type" => "work", + "value" => "jdoe@example.com" }], + "externalId" => external_user_id, + "groups" => [], + "id" => created_user.id.to_s, + "meta" => { "created" => created_user.created_at.iso8601, + "lastModified" => created_user.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{created_user.id}", + "resourceType" => "User" }, + "name" => { "familyName" => "Doe", + "givenName" => "John" }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => "jdoe") end end @@ -376,8 +435,9 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -398,21 +458,21 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do get "/scim_v2/Users/#{user.id}", "", headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "active" => false, - "emails" => [{ "primary" => true, - "type" => "work", - "value" => user.mail }], - "externalId" => external_user_id, - "groups" => [{ "value" => group.id.to_s }], - "id" => user.id.to_s, - "meta" => { "created" => user.created_at.iso8601, - "lastModified" => user.updated_at.iso8601, - "location" => "http://test.host/scim_v2/Users/#{user.id}", - "resourceType" => "User" }, - "name" => { "familyName" => user.lastname, - "givenName" => user.firstname }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], - "userName" => user.login }) + expect(response_body).to eq("active" => false, + "emails" => [{ "primary" => true, + "type" => "work", + "value" => user.mail }], + "externalId" => external_user_id, + "groups" => [{ "value" => group.id.to_s }], + "id" => user.id.to_s, + "meta" => { "created" => user.created_at.iso8601, + "lastModified" => user.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{user.id}", + "resourceType" => "User" }, + "name" => { "familyName" => user.lastname, + "givenName" => user.firstname }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => user.login) perform_enqueued_jobs assert_performed_jobs 1 @@ -421,9 +481,9 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Resource \"#{user.id}\" not found", - "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "404" } + "detail" => "Resource \"#{user.id}\" not found", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "404" ) end end @@ -434,9 +494,9 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do delete "/scim_v2/Users/#{user.id}", "", headers response_body = JSON.parse(last_response.body) - expect(response_body).to eq({ "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "detail" => "User can't be deleted due to permission absence.", - "status" => "403" }) + expect(response_body).to eq("schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "detail" => "Action forbidden: insufficient permissions.", + "status" => "403") expect(last_response).to have_http_status(403) end end @@ -448,8 +508,9 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -485,21 +546,21 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) user.reload - expect(response_body).to eq({ "active" => true, - "emails" => [{ "primary" => true, - "type" => "work", - "value" => request_body["emails"].first["value"] }], - "externalId" => new_external_user_id, - "groups" => [{ "value" => group.id.to_s }], - "id" => user.id.to_s, - "meta" => { "created" => user.created_at.iso8601, - "lastModified" => user.updated_at.iso8601, - "location" => "http://test.host/scim_v2/Users/#{user.id}", - "resourceType" => "User" }, - "name" => { "familyName" => request_body["name"]["familyName"], - "givenName" => request_body["name"]["givenName"] }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], - "userName" => request_body["userName"] }) + expect(response_body).to eq("active" => true, + "emails" => [{ "primary" => true, + "type" => "work", + "value" => request_body["emails"].first["value"] }], + "externalId" => new_external_user_id, + "groups" => [{ "value" => group.id.to_s }], + "id" => user.id.to_s, + "meta" => { "created" => user.created_at.iso8601, + "lastModified" => user.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{user.id}", + "resourceType" => "User" }, + "name" => { "familyName" => request_body["name"]["familyName"], + "givenName" => request_body["name"]["givenName"] }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => request_body["userName"]) end end @@ -509,8 +570,9 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end @@ -537,21 +599,21 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) user.reload - expect(response_body).to eq({ "active" => true, - "emails" => [{ "primary" => true, - "type" => "work", - "value" => user.mail }], - "externalId" => new_external_user_id, - "groups" => [{ "value" => group.id.to_s }], - "id" => user.id.to_s, - "meta" => { "created" => user.created_at.iso8601, - "lastModified" => user.updated_at.iso8601, - "location" => "http://test.host/scim_v2/Users/#{user.id}", - "resourceType" => "User" }, - "name" => { "familyName" => user.lastname, - "givenName" => user.firstname }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], - "userName" => user.login }) + expect(response_body).to eq("active" => true, + "emails" => [{ "primary" => true, + "type" => "work", + "value" => user.mail }], + "externalId" => new_external_user_id, + "groups" => [{ "value" => group.id.to_s }], + "id" => user.id.to_s, + "meta" => { "created" => user.created_at.iso8601, + "lastModified" => user.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{user.id}", + "resourceType" => "User" }, + "name" => { "familyName" => user.lastname, + "givenName" => user.firstname }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => user.login) end it "changes email value" do @@ -574,21 +636,21 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) user.reload - expect(response_body).to eq({ "active" => true, - "emails" => [{ "primary" => true, - "type" => "work", - "value" => new_email_value }], - "externalId" => user.scim_external_id, - "groups" => [{ "value" => group.id.to_s }], - "id" => user.id.to_s, - "meta" => { "created" => user.created_at.iso8601, - "lastModified" => user.updated_at.iso8601, - "location" => "http://test.host/scim_v2/Users/#{user.id}", - "resourceType" => "User" }, - "name" => { "familyName" => user.lastname, - "givenName" => user.firstname }, - "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], - "userName" => user.login }) + expect(response_body).to eq("active" => true, + "emails" => [{ "primary" => true, + "type" => "work", + "value" => new_email_value }], + "externalId" => user.scim_external_id, + "groups" => [{ "value" => group.id.to_s }], + "id" => user.id.to_s, + "meta" => { "created" => user.created_at.iso8601, + "lastModified" => user.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{user.id}", + "resourceType" => "User" }, + "name" => { "familyName" => user.lastname, + "givenName" => user.firstname }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => user.login) end end @@ -598,8 +660,9 @@ RSpec.describe "SCIM API Users", with_ee: [:scim_api] do response_body = JSON.parse(last_response.body) expect(response_body).to eq( - { "detail" => "Requires authentication", "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], - "status" => "401" } + "detail" => "Requires authentication", + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:Error"], + "status" => "401" ) expect(last_response).to have_http_status(401) end