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 Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ 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'
gem 'sdoc', git: 'https://github.com/pond/sdoc.git', branch: 'master'

gemspec
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Some aspects of configuration are handled via a `config/initializers/scimitar.rb
Rails.application.config.to_prepare do
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
# ...see subsections below for configuration options...
end
})
end
```

Expand Down Expand Up @@ -184,7 +184,36 @@ Note that Okta has some [curious documentation on its use of `POST` vs `PATCH` f

### Google Workspace note

Using SCIM with Google Workspace might only work for a subset of applications. Since web UIs for major service providers change very often, it doesn't make sense to provide extensive documentation here as it would get out of date quickly; you may have to figure out the setup as best you can using whatever current Google documentation exists for their system. There are [some notes which were relevant around mid-2025](https://github.com/pond/scimitar/issues/142#issuecomment-2699050541) (from when a workarond/fix was incorporated into Scimitar to allow it to work with Google Workspace) which may help you get started.
Using SCIM with Google Workspace might only work for a subset of applications. Since web UIs for major service providers change very often, it doesn't make sense to provide extensive documentation here as it would get out of date quickly; you may have to figure out the setup as best you can using whatever current Google documentation exists for their system. There are [some notes which were relevant around mid-2025](https://github.com/pond/scimitar/issues/142#issuecomment-2699050541) (from when a workaround/fix was incorporated into Scimitar to allow it to work with Google Workspace) which may help you get started.

### Request content type handling

The correct content type for SCIM is `application/scim+json`. Scimitar tolerates some variants of this, rewriting things internally so that the request continues to be processed by the rest of the gem (including ending up in subclass code you write) under this media type, with a Rails request format of `:scim`. Sometimes, callers into SCIM endpoints might use a content type that Scimitar rejects. If so, you can configure a custom request sanitizer Proc (with a "z", in keeping with Rails spelling of "sanitize"):

```ruby
Rails.application.config.to_prepare do
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({

custom_request_sanitizer: Proc.new do | request |
#
# Examine e.g. 'request.media_type' and evaluate to a Symbol:
#
# :success - set the standard content type and ensure the Rails request
# format is :scim; continue processing normally
#
# :preserve - retain the existing content type and Rails request format;
# continue processing normally
#
# :fail - the request format appears to be invalid; generate a 406
# ("Not Acceptable") response
#
end

})
end
```

The Proc is passed the [`ActionDispatch::Request`](https://api.rubyonrails.org/classes/ActionDispatch/Request.html) instance for the current request. It **must** evaluable to a Symbol as shown above. Typically, `:preserve` is only for very special use cases where you understand that the request headers and/or format might not match what other parts of Scimitar expect, but have written appropriate custom code elsewhere to deal with that.

### Data models

Expand Down
42 changes: 30 additions & 12 deletions app/controllers/scimitar/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,37 @@ def handle_unexpected_error(exception)
#
def require_scim
scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
failure_detail = "Only #{scim_mime_type} type is accepted."

if Scimitar.engine_configuration.custom_request_sanitizer.is_a?(Proc)

result = Scimitar.engine_configuration.custom_request_sanitizer.call(request)
case result
when :fail
handle_scim_error(ErrorResponse.new(status: 406, detail: failure_detail))
when :preserve
# Do nothing
else
request.format = :scim
request.headers['CONTENT_TYPE'] = scim_mime_type
end

else # "if Scimitar.engine_configuration.custom_request_sanitizer.present?"

if request.media_type.nil? || request.media_type.empty?
request.format = :scim
request.headers['CONTENT_TYPE'] = scim_mime_type
elsif request.media_type.downcase == scim_mime_type
request.format = :scim
elsif request.format == :scim
request.headers['CONTENT_TYPE'] = scim_mime_type
elsif request.media_type.downcase == 'application/json' && request.user_agent&.start_with?('Google') # https://github.com/pond/scimitar/issues/142
request.format = :scim
request.headers["CONTENT_TYPE"] = scim_mime_type
else
handle_scim_error(ErrorResponse.new(status: 406, detail: failure_detail))
end

if request.media_type.nil? || request.media_type.empty?
request.format = :scim
request.headers['CONTENT_TYPE'] = scim_mime_type
elsif request.media_type.downcase == scim_mime_type
request.format = :scim
elsif request.format == :scim
request.headers['CONTENT_TYPE'] = scim_mime_type
elsif request.media_type.downcase == 'application/json' && request.user_agent.start_with?('Google') # https://github.com/pond/scimitar/issues/142
request.format = :scim
request.headers["CONTENT_TYPE"] = scim_mime_type
else
handle_scim_error(ErrorResponse.new(status: 406, detail: "Only #{scim_mime_type} type is accepted."))
end
end

Expand Down
1 change: 1 addition & 0 deletions app/models/scimitar/engine_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class EngineConfiguration
:basic_authenticator,
:token_authenticator,
:custom_authenticator,
:custom_request_sanitizer,
:application_controller_mixin,
:exception_reporter,
:optional_value_fields_required,
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.13.0'
VERSION = '2.14.0'

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

end
4 changes: 2 additions & 2 deletions scimitar.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ Gem::Specification.new do |s|
end

s.add_development_dependency 'debug', '~> 1.11'
s.add_development_dependency 'rake', '~> 13.2'
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.14'
s.add_development_dependency 'rdoc', '~> 6.15'
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
55 changes: 55 additions & 0 deletions spec/controllers/scimitar/application_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,61 @@ def index; end
expect(@exception.message).to eql('Only application/scim+json type is accepted.')
end
end # "context 'and with Google SCIM calls' do"

context 'and with a custom request sanitizer' do
around :each do | example |
original_configuration = Scimitar.engine_configuration.custom_request_sanitizer
Scimitar.engine_configuration.custom_request_sanitizer = Proc.new do | request |
case request.media_type
when 'application/json+success'
:success
when 'application/json+preserve'
:preserve
else
:fail
end
end
example.run()
ensure
Scimitar.engine_configuration.custom_request_sanitizer = original_configuration
end

context 'returning "success"' do
it 'reaches the controller action with cleaned up request data' do
request.headers['Content-Type'] = 'application/json+success'
get :index

expect(@exception).to be_a(RuntimeError)
expect(@exception.message).to eql('Bang')

expect(request.format == :scim).to eql(true)
expect(request.headers['CONTENT_TYPE']).to eql('application/scim+json')
end
end # "context 'returning "success"' do"

context 'returning "preserve"' do
it 'reaches the controller action with unmodified request data' do
request.headers['Content-Type'] = 'application/json+preserve'
get :index

expect(@exception).to be_a(RuntimeError)
expect(@exception.message).to eql('Bang')

expect(request.format == :html).to eql(true)
expect(request.headers['CONTENT_TYPE']).to eql('application/json+preserve')
end
end # "context 'returning "keep"' do"

context 'returning "fail"' do
it 'is invoked' do
request.headers['Content-Type'] = 'application/json+fail'
get :index

expect(@exception).to be_a(Scimitar::ErrorResponse)
expect(@exception.message).to eql('Only application/scim+json type is accepted.')
end
end # "context 'returning "fail"' do"
end # "context 'and with a custom request sanitizer' do"
end # "context 'exception reporter' do"
end # "context 'error handling' do"
end