[#62107] Update SCIM Server API.

- Use ServiceAccount associated with ScimClient for making user changes
- Remove scoping by scim_client.auth_provider_id
  So, SCIM Client has access to any not_builtin User.
- Associate user with AuthProvider configured in ScimModel
  instead of choosing the first one.
This commit is contained in:
Pavel Balashou
2025-06-26 18:45:52 +02:00
parent 3b083cec26
commit 4e6f018f3e
7 changed files with 60 additions and 64 deletions
+4 -9
View File
@@ -38,7 +38,7 @@ module ScimV2
group = storage_class.new
group.from_scim!(scim_hash: scim_resource.as_json)
call = Groups::CreateService
.new(user: User.system, model: group)
.new(user: User.current, model: group)
.call(group.attributes)
.on_failure do |result|
uniqueness_error = result.errors.find { |e| e.type == :taken }
@@ -73,7 +73,7 @@ module ScimV2
group = storage_scope.find(group_id)
group.from_scim!(scim_hash: scim_resource.as_json)
Groups::UpdateService
.new(user: User.system, model: group)
.new(user: User.current, model: group)
.call(user_ids: scim_resource.members.map(&:value))
.on_failure { |call| raise call.message }
group.reload
@@ -89,7 +89,7 @@ module ScimV2
group.from_scim_patch!(patch_hash: patch_hash)
user_ids = group.scim_members.map(&:id)
Groups::UpdateService
.new(user: User.system, model: group)
.new(user: User.current, model: group)
.call(user_ids:)
.on_failure { |call| raise call.message }
group.reload
@@ -102,7 +102,7 @@ module ScimV2
super do |group_id|
group = storage_scope.find(group_id)
Groups::DeleteService
.new(user: User.system, model: group)
.new(user: User.current, model: group)
.call
.on_failure { |call| raise call.message }
end
@@ -118,12 +118,7 @@ module ScimV2
Group
.left_joins(:users, :user_auth_provider_links)
.includes(:users, :user_auth_provider_links)
.where(user_auth_provider_links: { auth_provider_id: scim_client.auth_provider_id })
.not_builtin
end
def scim_client
User.current.service_account_association.service
end
end
end
+4 -9
View File
@@ -40,7 +40,7 @@ module ScimV2
user.firstname = "123" if user.firstname.blank?
user.lastname = "456" if user.lastname.blank?
call = Users::CreateService
.new(user: User.system, model: user)
.new(user: User.current, model: user)
.call(user.attributes)
.on_failure do |result|
uniqueness_error = result.errors.find { |e| e.type == :taken }
@@ -67,7 +67,7 @@ module ScimV2
user = storage_scope.find(user_id)
user.from_scim!(scim_hash: scim_resource.as_json)
Users::UpdateService
.new(user: User.system, model: user)
.new(user: User.current, model: user)
.call
.on_failure { |call| raise call.message }
user.to_scim(location: url_for(action: :show, id: user.id))
@@ -81,7 +81,7 @@ module ScimV2
user = storage_scope.find(user_id)
user.from_scim_patch!(patch_hash: patch_hash)
Users::UpdateService
.new(user: User.system, model: user)
.new(user: User.current, model: user)
.call
.on_failure { |call| raise call.message }
user.to_scim(location: url_for(action: :show, id: user.id))
@@ -93,7 +93,7 @@ module ScimV2
super do |user_id|
user = storage_scope.find(user_id)
Users::DeleteService
.new(user: User.system, model: user)
.new(user: User.current, model: user)
.call
.on_failure { |call| raise call.message }
end
@@ -109,12 +109,7 @@ module ScimV2
User
.left_joins(:groups, :user_auth_provider_links)
.includes(:groups, :user_auth_provider_links)
.where(user_auth_provider_links: { auth_provider_id: scim_client.auth_provider_id })
.not_builtin
end
def scim_client
User.current.service_account_association.service
end
end
end
+1 -2
View File
@@ -74,8 +74,7 @@ class Group < Principal
end
def scim_external_id=(external_id)
oidc_provider = OpenIDConnect::Provider.first
raise "There should at least one OIDC Provider for SCIM to work with" unless oidc_provider
oidc_provider = User.current.service_account_association.service.auth_provider
::Groups::SetAttributesService
.new(user: User.system, model: self, contract_class: EmptyContract)
+1 -2
View File
@@ -563,8 +563,7 @@ class User < Principal
end
def scim_external_id=(external_id)
oidc_provider = OpenIDConnect::Provider.first
raise "There should at least one OIDC Provider for SCIM to work with" unless oidc_provider
oidc_provider = User.current.service_account_association.service.auth_provider
::Users::SetAttributesService
.new(user: User.system, model: self, contract_class: EmptyContract)
+1 -1
View File
@@ -31,7 +31,7 @@
FactoryBot.define do
factory :user, parent: :principal, class: "User" do
firstname { "Bob" }
lastname { "Bobbit" }
sequence(:lastname) { |n| "Bobbit#{n}" }
sequence(:login) { |n| "bob#{n}" }
sequence(:mail) { |n| "bobmail#{n}.bobbit@bob.com" }
password { "adminADMIN!" }
+13 -4
View File
@@ -44,7 +44,7 @@ RSpec.describe "SCIM API Groups" do
let(:group_without_external_id) { create(:group, members: [user]) }
let(:headers) { { "CONTENT_TYPE" => "application/scim+json", "HTTP_AUTHORIZATION" => "Bearer #{token.plaintext_token}" } }
let(:token) { create(:oauth_access_token, resource_owner: service_account, scopes: ["scim_v2"]) }
let(:service_account) { create(:service_account, service: scim_client) }
let(:service_account) { create(:service_account, service: scim_client, admin: true) }
let(:scim_client) { create(:scim_client, authentication_method: :oauth2_token, auth_provider_id: oidc_provider.id) }
before do
@@ -61,7 +61,7 @@ RSpec.describe "SCIM API Groups" do
get "/scim_v2/Groups", {}, headers
response_body = JSON.parse(last_response.body)
expect(response_body).to eq({ "Resources" => [{ "displayName" => group.name,
expect(response_body).to match({ "Resources" => match_array([{ "displayName" => group.name,
"externalId" => external_group_id,
"id" => group.id.to_s,
"members" => [{ "value" => user.id.to_s }],
@@ -69,11 +69,20 @@ RSpec.describe "SCIM API Groups" do
"created" => group.created_at.iso8601,
"lastModified" => group.updated_at.iso8601,
"resourceType" => "Group" },
"schemas" => ["urn:ietf:params:scim:schemas:core:2.0: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" => 1 })
"totalResults" => 2 })
end
it "filters results" do
+36 -37
View File
@@ -41,7 +41,7 @@ RSpec.describe "SCIM API Users" do
let(:group) { create(:group, identity_url: "#{oidc_provider.slug}:#{external_group_id}", members: [user]) }
let(:headers) { { "CONTENT_TYPE" => "application/scim+json", "HTTP_AUTHORIZATION" => "Bearer #{token.plaintext_token}" } }
let(:token) { create(:oauth_access_token, resource_owner: service_account, scopes: ["scim_v2"]) }
let(:service_account) { create(:service_account, service: scim_client) }
let(:service_account) { create(:service_account, service: scim_client, admin: true) }
let(:scim_client) { create(:scim_client, authentication_method: :oauth2_token, auth_provider_id: oidc_provider.id) }
before { token }
@@ -57,41 +57,40 @@ RSpec.describe "SCIM API Users" 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 }],
"groups" => [],
"externalId" => external_admin_id,
"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)
expect(response_body).to match("Resources" => include({ "active" => true,
"emails" => [{ "primary" => true,
"type" => "work",
"value" => admin.mail }],
"groups" => [],
"externalId" => external_admin_id,
"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" => 4)
end
it "filters results by familyName" do
@@ -345,7 +344,7 @@ RSpec.describe "SCIM API Users" do
end
end
describe "DELETE /scim_v2/Users/:id" do
describe "DELETE /scim_v2/Users/:id", with_settings: { users_deletable_by_admins: true } do
context "with the feature flag enabled", with_flag: { scim_api: true } do
it do
group