Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
name: Ruby ${{ matrix.ruby }}
strategy:
matrix:
ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4']
ruby: ['2.7', '3.2', '3.3', '3.4', '4.0']

services:
postgres:
Expand Down
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.4.2
4.0.1
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# 2.15.0 (2026-03-06)

Fixes:

* Supports SCIM 2.0 `count=0` parameter (RFC 7644 compliance improvement) via [#168](https://github.com/pond/scimitar/pull/168) - thanks to `@lorman`

Features:

* Ruby 4.0.1 added to the test matrix and therefore 'officially' supported
* New engine configuration option `render_mapped_nil_values_in_response` allows omission of `nil` source value items from a SCIM representation, with some limitations; aims to solve [#170](https://github.com/pond/scimitar/issues/170) reported by `@xanderman`, but might need further iteration
* Controller methods can be accessed in the `exception_reporter` Proc via [#167](https://github.com/pond/scimitar/pull/167) - thanks to `@bcroesch`

# 2.14.0 (2025-11-14)

Features:
Expand Down
5 changes: 0 additions & 5 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
source "https://rubygems.org"

# Use a fork version of SDoc; can't use ":git" in ".gemspec" files, so do
# it here instead.
#
gem 'sdoc', git: 'https://github.com/pond/sdoc.git', branch: 'master'

gemspec
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 RIPA Global
Copyright (c) 2026 RIPA Global c/o Andrew Hodgkinson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
3 changes: 1 addition & 2 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require 'rake'
require 'rspec/core/rake_task'
require 'rdoc/task'
require 'sdoc'

RSpec::Core::RakeTask.new(:default) do | t |
end
Expand All @@ -12,5 +11,5 @@ Rake::RDocTask.new do | rd |
rd.title = 'Scimitar'
rd.main = 'README.md'
rd.rdoc_dir = 'docs/rdoc'
rd.generator = 'sdoc'
rd.generator = 'rdoc'
end
6 changes: 4 additions & 2 deletions app/models/scimitar/engine_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class EngineConfiguration
:application_controller_mixin,
:exception_reporter,
:optional_value_fields_required,
:render_mapped_nil_values_in_response,
:schema_list_from_attribute_mappings,
)

Expand All @@ -25,8 +26,9 @@ def initialize(attributes = {})
# Set defaults that may be overridden by the initializer.
#
defaults = {
optional_value_fields_required: true,
schema_list_from_attribute_mappings: []
optional_value_fields_required: true,
render_mapped_nil_values_in_response: true,
schema_list_from_attribute_mappings: []
}

super(defaults.merge(attributes))
Expand Down
8 changes: 7 additions & 1 deletion app/models/scimitar/resources/mixin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,13 @@ def to_scim_backend(
end
end

result.compact! if include_attributes.any?
if (
include_attributes.any? or
! Scimitar.engine_configuration.render_mapped_nil_values_in_response
)
result.compact!
end

result

when Array # Static or dynamic mapping against lists in data source
Expand Down
6 changes: 6 additions & 0 deletions config/initializers/scimitar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@
#
# optional_value_fields_required: false

# When rendering responses, +nil+ values can either still be included via
# the attributes map with a JSON value of +null+, or omitted. By default,
# all attributes in your map are returned in responses.
#
# render_mapped_nil_values_in_response: false

# The SCIM standard `/Schemas` endpoint lists, by default, all known schema
# definitions with the mutabilty (read-write, read-only, write-only) state
# described by those definitions, and includes all defined attributes. For
Expand Down
4 changes: 2 additions & 2 deletions lib/scimitar/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ module Scimitar
# Gem version. If this changes, be sure to re-run "bundle install" or
# "bundle update".
#
VERSION = '2.14.0'
VERSION = '2.15.0'

# Date for VERSION. If this changes, be sure to re-run "bundle install"
# or "bundle update".
#
DATE = '2025-11-14'
DATE = '2026-03-06'

end
4 changes: 2 additions & 2 deletions scimitar.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Gem::Specification.new do |s|
s.date = Scimitar::DATE
s.summary = 'SCIM v2 for Rails'
s.description = 'SCIM v2 support for Users and Groups in Ruby On Rails'
s.authors = ['RIPA Global', 'Andrew David Hodgkinson']
s.authors = ['Andrew David Hodgkinson', 'RIPA Global']
s.email = ['ahodgkin@rowing.org.uk']
s.license = 'MIT'

Expand Down Expand Up @@ -37,7 +37,7 @@ Gem::Specification.new do |s|
s.add_development_dependency 'rake', '~> 13.3'
s.add_development_dependency 'pg', '~> 1.6'
s.add_development_dependency 'simplecov-rcov', '~> 0.3'
s.add_development_dependency 'rdoc', '~> 6.15'
s.add_development_dependency 'rdoc', '~> 7.2'
s.add_development_dependency 'warden', '~> 1.2'
s.add_development_dependency 'rspec-rails', '~> 7.1'
s.add_development_dependency 'warden-rspec-rails', '~> 0.3'
Expand Down
112 changes: 110 additions & 2 deletions spec/models/scimitar/resources/mixin_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def self.scim_queryable_attributes
instance.first_name = 'Foo'
instance.last_name = 'Bar'
instance.work_email_address = 'foo.bar@test.com'
instance.home_email_address = nil
instance.home_email_address = 'foo.bar@example.com'
instance.work_phone_number = '+642201234567'
instance.organization = 'SOMEORG'

Expand Down Expand Up @@ -299,6 +299,49 @@ def self.scim_queryable_attributes
'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {},
})
end

it 'hides "nil" value attributes' do
uuid = SecureRandom.uuid

instance = MockUser.new
instance.primary_key = uuid
instance.scim_uid = 'AA02984'
instance.username = nil
instance.password = 'correcthorsebatterystaple'
instance.first_name = nil
instance.last_name = 'Bar'
instance.work_email_address = 'foo.bar@test.com'
instance.home_email_address = 'foo.bar@example.com'
instance.work_phone_number = '+642201234567'
instance.organization = 'SOMEORG'

g1 = MockGroup.create!(display_name: 'Group 1')
g2 = MockGroup.create!(display_name: 'Group 2')
g3 = MockGroup.create!(display_name: 'Group 3')

g1.mock_users << instance
g3.mock_users << instance

scim = instance.to_scim(location: "https://test.com/mock_users/#{uuid}", include_attributes: %w[id userName name groups.display groups.value organization])
json = scim.to_json()
hash = JSON.parse(json)

expect(hash).to eql({
'id' => uuid,
'name' => {'familyName'=>'Bar'},
'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
'schemas' => [
'urn:ietf:params:scim:schemas:core:2.0:User',
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
'urn:ietf:params:scim:schemas:extension:manager:1.0:User',
],
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
'organization' => 'SOMEORG',
},
'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {},
})
end
end # "context 'with list of requested attributes' do"

context 'with a UUID, renamed primary key column' do
Expand Down Expand Up @@ -332,7 +375,7 @@ def self.scim_queryable_attributes
'userName' => 'foo',
'name' => {'givenName'=>'Foo', 'familyName'=>'Bar'},
'active' => true,
'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {"primary"=>false, "type"=>"home", "value"=>nil}],
'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {'primary'=>false, 'type'=>'home', 'value'=>nil}],
'phoneNumbers'=> [{'type'=>'work', 'primary'=>false, 'value'=>'+642201234567'}],
'id' => uuid,
'externalId' => 'AA02984',
Expand All @@ -353,6 +396,71 @@ def self.scim_queryable_attributes
},
})
end

context 'and when configured to omit "nil" values in the response' do
around :each do | example |
original_configuration = Scimitar.engine_configuration.render_mapped_nil_values_in_response
Scimitar.engine_configuration.render_mapped_nil_values_in_response = false
example.run()
ensure
Scimitar.engine_configuration.render_mapped_nil_values_in_response = original_configuration
end

it 'omits "nil" values as expected' do
uuid = SecureRandom.uuid

instance = MockUser.new
instance.primary_key = uuid
instance.scim_uid = 'AA02984'
instance.username = 'foo'
instance.password = 'correcthorsebatterystaple'
instance.first_name = nil
instance.last_name = 'Bar'
instance.work_email_address = 'foo.bar@test.com'
instance.home_email_address = nil
instance.work_phone_number = '+642201234567'
instance.organization = 'SOMEORG'

g1 = MockGroup.create!(display_name: 'Group 1')
g2 = MockGroup.create!(display_name: 'Group 2')
g3 = MockGroup.create!(display_name: 'Group 3')

g1.mock_users << instance
g3.mock_users << instance

scim = instance.to_scim(location: "https://test.com/mock_users/#{uuid}")
json = scim.to_json()
hash = JSON.parse(json)

# Note currently limited implementation for things like static maps,
# where part of the returned value is included; in this case, the
# "primary" value is "false" for e-mail of type "home", so the
# structure for that *does* appear in the output array even though
# the source dynamic data field from the NockUser instance is "nil".
#
expect(hash).to eql({
'userName' => 'foo',
'name' => {'familyName'=>'Bar'},
'active' => true,
'emails' => [{'type'=>'work', 'primary'=>true, 'value'=>'foo.bar@test.com'}, {'primary' => false, 'type' => 'home'}],
'phoneNumbers'=> [{'type'=>'work', 'primary'=>false, 'value'=>'+642201234567'}],
'id' => uuid,
'externalId' => 'AA02984',
'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
'schemas' => [
'urn:ietf:params:scim:schemas:core:2.0:User',
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
'urn:ietf:params:scim:schemas:extension:manager:1.0:User',
],
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
'organization' => 'SOMEORG',
'primaryEmail' => instance.work_email_address,
},
'urn:ietf:params:scim:schemas:extension:manager:1.0:User' => {}
})
end
end # "context 'and when configured to omit "nil" values in the response'" do"
end # "context 'with a UUID, renamed primary key column' do"

context 'with an integer, conventionally named primary key column' do
Expand Down
Loading