From 6079a6658cbeb4e08639ab22cb709ff0969d2573 Mon Sep 17 00:00:00 2001 From: Andrew Hodgkinson Date: Fri, 6 Mar 2026 11:49:10 +1300 Subject: [PATCH] Address #170, general maintenance, prepare for v2.15.0 --- .github/workflows/main.yml | 2 +- .ruby-version | 2 +- CHANGELOG.md | 12 ++ Gemfile | 5 - LICENSE.txt | 2 +- Rakefile | 3 +- app/models/scimitar/engine_configuration.rb | 6 +- app/models/scimitar/resources/mixin.rb | 8 +- config/initializers/scimitar.rb | 6 + lib/scimitar/version.rb | 4 +- scimitar.gemspec | 4 +- spec/models/scimitar/resources/mixin_spec.rb | 112 ++++++++++++++++++- 12 files changed, 147 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index df136a9..1b51a91 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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: diff --git a/.ruby-version b/.ruby-version index 4d9d11c..1454f6e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.2 +4.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e673d9..224ef09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/Gemfile b/Gemfile index 38ba752..b4e2a20 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/LICENSE.txt b/LICENSE.txt index b14d1fc..4b1a846 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -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 diff --git a/Rakefile b/Rakefile index 40b001c..a3040b1 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,6 @@ require 'rake' require 'rspec/core/rake_task' require 'rdoc/task' -require 'sdoc' RSpec::Core::RakeTask.new(:default) do | t | end @@ -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 diff --git a/app/models/scimitar/engine_configuration.rb b/app/models/scimitar/engine_configuration.rb index 605d51c..fca9dcc 100644 --- a/app/models/scimitar/engine_configuration.rb +++ b/app/models/scimitar/engine_configuration.rb @@ -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, ) @@ -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)) diff --git a/app/models/scimitar/resources/mixin.rb b/app/models/scimitar/resources/mixin.rb index bfe184c..1272992 100644 --- a/app/models/scimitar/resources/mixin.rb +++ b/app/models/scimitar/resources/mixin.rb @@ -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 diff --git a/config/initializers/scimitar.rb b/config/initializers/scimitar.rb index c84da90..80d67e8 100644 --- a/config/initializers/scimitar.rb +++ b/config/initializers/scimitar.rb @@ -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 diff --git a/lib/scimitar/version.rb b/lib/scimitar/version.rb index 142b9b2..b4ab01c 100644 --- a/lib/scimitar/version.rb +++ b/lib/scimitar/version.rb @@ -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 diff --git a/scimitar.gemspec b/scimitar.gemspec index 71dd3e1..12f8651 100644 --- a/scimitar.gemspec +++ b/scimitar.gemspec @@ -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' @@ -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' diff --git a/spec/models/scimitar/resources/mixin_spec.rb b/spec/models/scimitar/resources/mixin_spec.rb index 59d0a7c..6e197b2 100644 --- a/spec/models/scimitar/resources/mixin_spec.rb +++ b/spec/models/scimitar/resources/mixin_spec.rb @@ -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' @@ -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 @@ -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', @@ -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