diff --git a/Gemfile.lock b/Gemfile.lock index ffe0ddcfba7..119b94afb43 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1506,7 +1506,7 @@ DEPENDENCIES ruby-progressbar (~> 1.13.0) rubytree (~> 2.1.0) sanitize (~> 7.0.0) - scimitar + scimitar (~> 2.11) secure_headers (~> 7.1.0) selenium-devtools selenium-webdriver (~> 4.20) diff --git a/app/controllers/scim_v2/base_controller_actions.rb b/app/controllers/scim_v2/base_controller_actions.rb new file mode 100644 index 00000000000..de58897c464 --- /dev/null +++ b/app/controllers/scim_v2/base_controller_actions.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module ScimV2 + module BaseControllerActions + extend ActiveSupport::Concern + + included do + skip_before_action :verify_authenticity_token + + rescue_from "ActiveRecord::RecordNotFound", with: :handle_resource_not_found + + def index + query = if params[:filter].blank? + storage_scope + else + attribute_map = storage_class.new.scim_queryable_attributes + parser = ::Scimitar::Lists::QueryParser.new(attribute_map) + + parser.parse(params[:filter]) + parser.to_activerecord_query(storage_scope) + end + + pagination_info = scim_pagination_info(query.count) + page_of_results = query + .order(id: :asc) + .offset(pagination_info.offset) + .limit(pagination_info.limit) + .to_a + + super(pagination_info, page_of_results) do |record| + record.to_scim(location: url_for(action: :show, id: record.id)) + end + end + + def show + super do |record_id| + record = storage_scope.find(record_id) + record.to_scim(location: url_for(action: :show, id: record_id)) + end + end + end + end +end diff --git a/app/controllers/scim_v2/groups_controller.rb b/app/controllers/scim_v2/groups_controller.rb index 6ab739a5123..0f88525b368 100644 --- a/app/controllers/scim_v2/groups_controller.rb +++ b/app/controllers/scim_v2/groups_controller.rb @@ -1,40 +1,36 @@ # frozen_string_literal: true +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + module ScimV2 class GroupsController < Scimitar::ResourcesController - skip_before_action :verify_authenticity_token - - rescue_from "ActiveRecord::RecordNotFound", with: :handle_resource_not_found - - def index - query = if params[:filter].blank? - storage_scope - else - attribute_map = storage_class.new.scim_queryable_attributes - parser = ::Scimitar::Lists::QueryParser.new(attribute_map) - - parser.parse(params[:filter]) - parser.to_activerecord_query(storage_scope) - end - - pagination_info = scim_pagination_info(query.count) - page_of_results = query - .order(id: :asc) - .offset(pagination_info.offset) - .limit(pagination_info.limit) - .to_a - - super(pagination_info, page_of_results) do |record| - record.to_scim(location: url_for(action: :show, id: record.id)) - end - end - - def show - super do |group_id| - group = storage_scope.find(group_id) - group.to_scim(location: url_for(action: :show, id: group_id)) - end - end + include BaseControllerActions def create super do |scim_resource| diff --git a/app/controllers/scim_v2/users_controller.rb b/app/controllers/scim_v2/users_controller.rb index 566d6cc0e0f..fa9103906ab 100644 --- a/app/controllers/scim_v2/users_controller.rb +++ b/app/controllers/scim_v2/users_controller.rb @@ -1,40 +1,36 @@ # frozen_string_literal: true +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + module ScimV2 class UsersController < Scimitar::ResourcesController - skip_before_action :verify_authenticity_token - - rescue_from "ActiveRecord::RecordNotFound", with: :handle_resource_not_found - - def index - query = if params[:filter].blank? - storage_scope - else - attribute_map = storage_class.new.scim_queryable_attributes - parser = ::Scimitar::Lists::QueryParser.new(attribute_map) - - parser.parse(params[:filter]) - parser.to_activerecord_query(storage_scope) - end - - pagination_info = scim_pagination_info(query.count) - page_of_results = query - .order(id: :asc) - .offset(pagination_info.offset) - .limit(pagination_info.limit) - .to_a - - super(pagination_info, page_of_results) do |record| - record.to_scim(location: url_for(action: :show, id: record.id)) - end - end - - def show - super do |user_id| - user = storage_scope.find(user_id) - user.to_scim(location: url_for(action: :show, id: user_id)) - end - end + include BaseControllerActions def create super do |scim_resource| @@ -97,7 +93,7 @@ module ScimV2 end def storage_scope - User.user.where.not(identity_url: [nil, ""]) + User.not_builtin end end end diff --git a/app/models/group.rb b/app/models/group.rb index 7d9016cb0b7..5be997277bd 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -111,7 +111,7 @@ class Group < Principal case type.downcase when "user" - User.user.where.not(identity_url: [nil, ""]).find_by(id:) + User.not_builtin.find_by(id:) when "group" # TODO OP does not support nesting of groups but SCIM does. # For now raises exception in case of group as a member arrival. @@ -130,7 +130,7 @@ class Group < Principal def self.scim_queryable_attributes { - displayName: :name + displayName: { column: :lastname } } end diff --git a/spec/requests/scim_v2/groups_spec.rb b/spec/requests/scim_v2/groups_spec.rb index b052dc938dc..262ed35a2cc 100644 --- a/spec/requests/scim_v2/groups_spec.rb +++ b/spec/requests/scim_v2/groups_spec.rb @@ -62,25 +62,35 @@ RSpec.describe "SCIM API Groups" do "totalResults" => 1 }) end - # it do - # filter = ERB::Util.url_encode('displayName Eq "john"') - # get "/scim_v2/Groups?filter=#{filter}", {}, headers + it "filters results" do + filter = ERB::Util.url_encode('displayName Eq "' + group.name + '"') + 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 }) - # end + 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 }) + + 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}) + end end context "with the feature flag disabled", with_flag: { scim_api: false } do @@ -265,7 +275,7 @@ RSpec.describe "SCIM API Groups" do new_external_group_id = "new_idp_user_id_123asdqwe12345" request_body = { "schemas" => - ["urn =>ietf:params:scim:api:messages:2.0:PatchOp"], + ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations" => [{ "op" => "replace", "path" => "externalId", diff --git a/spec/requests/scim_v2/users_spec.rb b/spec/requests/scim_v2/users_spec.rb index 652bede2350..608d1043c0e 100644 --- a/spec/requests/scim_v2/users_spec.rb +++ b/spec/requests/scim_v2/users_spec.rb @@ -41,11 +41,54 @@ RSpec.describe "SCIM API Users" do describe "GET /scim_v2/Users" do context "with the feature flag enabled", with_flag: { scim_api: true } do - it do - group + before { group } + it do get "/scim_v2/Users", {}, headers + response_body = JSON.parse(last_response.body) + expect(response_body).to eq("Resources" => + [{ "active" => true, + "emails" => [{ "primary" => true, + "type" => "work", + "value" => admin.mail }], + "externalId" => nil, + "groups" => [], + "id" => admin.id.to_s, + "meta" => { "created" => admin.created_at.iso8601, + "lastModified" => admin.updated_at.iso8601, + "location" => "http://test.host/scim_v2/Users/#{admin.id}", + "resourceType" => "User" }, + "name" => { "familyName" => admin.lastname, + "givenName" => admin.firstname }, + "schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName" => admin.login }, + { "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 }, + ], + "itemsPerPage" => 100, + "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + "startIndex" => 1, + "totalResults" => 2) + end + + it "filters results" do + filter_with_existing_rows = ERB::Util.url_encode('familyName Eq "' + user.lastname + '"') + get "/scim_v2/Users?filter=#{filter_with_existing_rows}", {}, headers + response_body = JSON.parse(last_response.body) expect(response_body).to eq("Resources" => [{ "active" => true, "emails" => [{ "primary" => true, @@ -66,7 +109,18 @@ RSpec.describe "SCIM API Users" do "schemas" => ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], "startIndex" => 1, "totalResults" => 1) + + filter_with_nonexisting_rows = ERB::Util.url_encode('familyName Eq "NONEXISTENT USER LASTNAME"') + 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}) end + end context "with the feature flag disabled", with_flag: { scim_api: false } do @@ -303,7 +357,7 @@ RSpec.describe "SCIM API Users" do it "changes external_id" do request_body = { "schemas" => - ["urn =>ietf:params:scim:api:messages:2.0:PatchOp"], + ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations" => [{ "op" => "replace", "path" => "externalId", @@ -335,7 +389,7 @@ RSpec.describe "SCIM API Users" do new_email_value = "qwertty@gmail.com" request_body = { "schemas" => - ["urn =>ietf:params:scim:api:messages:2.0:PatchOp"], + ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations" => [{ "op" => "replace", "path" => "emails[type eq \"work\"]",