powerhome-scimitar 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +708 -0
- data/Rakefile +16 -0
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +257 -0
- data/app/controllers/scimitar/application_controller.rb +157 -0
- data/app/controllers/scimitar/resource_types_controller.rb +28 -0
- data/app/controllers/scimitar/resources_controller.rb +203 -0
- data/app/controllers/scimitar/schemas_controller.rb +21 -0
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
- data/app/models/scimitar/authentication_error.rb +9 -0
- data/app/models/scimitar/authentication_scheme.rb +18 -0
- data/app/models/scimitar/bulk.rb +8 -0
- data/app/models/scimitar/complex_types/address.rb +12 -0
- data/app/models/scimitar/complex_types/base.rb +83 -0
- data/app/models/scimitar/complex_types/email.rb +12 -0
- data/app/models/scimitar/complex_types/entitlement.rb +12 -0
- data/app/models/scimitar/complex_types/ims.rb +12 -0
- data/app/models/scimitar/complex_types/name.rb +12 -0
- data/app/models/scimitar/complex_types/phone_number.rb +12 -0
- data/app/models/scimitar/complex_types/photo.rb +12 -0
- data/app/models/scimitar/complex_types/reference_group.rb +12 -0
- data/app/models/scimitar/complex_types/reference_member.rb +12 -0
- data/app/models/scimitar/complex_types/role.rb +12 -0
- data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
- data/app/models/scimitar/engine_configuration.rb +32 -0
- data/app/models/scimitar/error_response.rb +32 -0
- data/app/models/scimitar/errors.rb +14 -0
- data/app/models/scimitar/filter.rb +11 -0
- data/app/models/scimitar/filter_error.rb +22 -0
- data/app/models/scimitar/invalid_syntax_error.rb +9 -0
- data/app/models/scimitar/lists/count.rb +64 -0
- data/app/models/scimitar/lists/query_parser.rb +745 -0
- data/app/models/scimitar/meta.rb +7 -0
- data/app/models/scimitar/not_found_error.rb +10 -0
- data/app/models/scimitar/resource_invalid_error.rb +9 -0
- data/app/models/scimitar/resource_type.rb +29 -0
- data/app/models/scimitar/resources/base.rb +190 -0
- data/app/models/scimitar/resources/group.rb +13 -0
- data/app/models/scimitar/resources/mixin.rb +1524 -0
- data/app/models/scimitar/resources/user.rb +13 -0
- data/app/models/scimitar/schema/address.rb +25 -0
- data/app/models/scimitar/schema/attribute.rb +132 -0
- data/app/models/scimitar/schema/base.rb +90 -0
- data/app/models/scimitar/schema/derived_attributes.rb +24 -0
- data/app/models/scimitar/schema/email.rb +10 -0
- data/app/models/scimitar/schema/entitlement.rb +10 -0
- data/app/models/scimitar/schema/group.rb +27 -0
- data/app/models/scimitar/schema/ims.rb +10 -0
- data/app/models/scimitar/schema/name.rb +20 -0
- data/app/models/scimitar/schema/phone_number.rb +10 -0
- data/app/models/scimitar/schema/photo.rb +10 -0
- data/app/models/scimitar/schema/reference_group.rb +23 -0
- data/app/models/scimitar/schema/reference_member.rb +21 -0
- data/app/models/scimitar/schema/role.rb +10 -0
- data/app/models/scimitar/schema/user.rb +52 -0
- data/app/models/scimitar/schema/vdtp.rb +18 -0
- data/app/models/scimitar/schema/x509_certificate.rb +22 -0
- data/app/models/scimitar/service_provider_configuration.rb +60 -0
- data/app/models/scimitar/supportable.rb +14 -0
- data/app/views/layouts/scimitar/application.html.erb +14 -0
- data/config/initializers/scimitar.rb +111 -0
- data/config/routes.rb +6 -0
- data/lib/scimitar/engine.rb +63 -0
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +216 -0
- data/lib/scimitar/support/utilities.rb +51 -0
- data/lib/scimitar/version.rb +13 -0
- data/lib/scimitar.rb +29 -0
- data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
- data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
- data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
- data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
- data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
- data/spec/apps/dummy/app/models/mock_group.rb +83 -0
- data/spec/apps/dummy/app/models/mock_user.rb +132 -0
- data/spec/apps/dummy/config/application.rb +18 -0
- data/spec/apps/dummy/config/boot.rb +2 -0
- data/spec/apps/dummy/config/environment.rb +2 -0
- data/spec/apps/dummy/config/environments/test.rb +38 -0
- data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/apps/dummy/config/initializers/scimitar.rb +61 -0
- data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
- data/spec/apps/dummy/config/routes.rb +45 -0
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +24 -0
- data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +13 -0
- data/spec/apps/dummy/db/schema.rb +48 -0
- data/spec/controllers/scimitar/application_controller_spec.rb +296 -0
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
- data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +83 -0
- data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
- data/spec/models/scimitar/complex_types/address_spec.rb +18 -0
- data/spec/models/scimitar/complex_types/email_spec.rb +21 -0
- data/spec/models/scimitar/lists/count_spec.rb +147 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +830 -0
- data/spec/models/scimitar/resource_type_spec.rb +21 -0
- data/spec/models/scimitar/resources/base_spec.rb +485 -0
- data/spec/models/scimitar/resources/base_validation_spec.rb +86 -0
- data/spec/models/scimitar/resources/mixin_spec.rb +3562 -0
- data/spec/models/scimitar/resources/user_spec.rb +68 -0
- data/spec/models/scimitar/schema/attribute_spec.rb +99 -0
- data/spec/models/scimitar/schema/base_spec.rb +64 -0
- data/spec/models/scimitar/schema/group_spec.rb +87 -0
- data/spec/models/scimitar/schema/user_spec.rb +720 -0
- data/spec/requests/active_record_backed_resources_controller_spec.rb +1354 -0
- data/spec/requests/application_controller_spec.rb +61 -0
- data/spec/requests/controller_configuration_spec.rb +17 -0
- data/spec/requests/engine_spec.rb +45 -0
- data/spec/spec_helper.rb +101 -0
- data/spec/spec_helper_spec.rb +30 -0
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +169 -0
- metadata +321 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
# SCIMITAR CONFIGURATION
|
2
|
+
#
|
3
|
+
# For supporting information and rationale, please see README.md.
|
4
|
+
|
5
|
+
Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
6
|
+
|
7
|
+
# ===========================================================================
|
8
|
+
# SERVICE PROVIDER CONFIGURATION
|
9
|
+
# ===========================================================================
|
10
|
+
#
|
11
|
+
# This is a Ruby abstraction over a SCIM entity that declares the
|
12
|
+
# capabilities supported by a particular implementation.
|
13
|
+
#
|
14
|
+
# Typically this is used to declare parts of the standard unsupported, if you
|
15
|
+
# don't need them and don't want to provide subclass support.
|
16
|
+
#
|
17
|
+
Scimitar.service_provider_configuration = Scimitar::ServiceProviderConfiguration.new({
|
18
|
+
|
19
|
+
# See https://tools.ietf.org/html/rfc7643#section-8.5 for properties.
|
20
|
+
#
|
21
|
+
# See Gem file 'app/models/scimitar/service_provider_configuration.rb'
|
22
|
+
# for defaults. Define Hash keys here that override defaults; e.g. to
|
23
|
+
# declare that filters are not supported so that calling clients shouldn't
|
24
|
+
# use them:
|
25
|
+
#
|
26
|
+
# filter: Scimitar::Supported.unsupported
|
27
|
+
|
28
|
+
})
|
29
|
+
|
30
|
+
# ===========================================================================
|
31
|
+
# ENGINE CONFIGURATION
|
32
|
+
# ===========================================================================
|
33
|
+
#
|
34
|
+
# This is where you provide callbacks for things like authorisation or mixins
|
35
|
+
# that get included into all Scimitar-derived controllers (for things like
|
36
|
+
# before-actions that apply to all Scimitar controller-based routes).
|
37
|
+
#
|
38
|
+
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
39
|
+
|
40
|
+
# If you have filters you want to run for any Scimitar action/route, you
|
41
|
+
# can define them here. You can also override any shared controller methods
|
42
|
+
# here. For example, you might use a before-action to set up some
|
43
|
+
# multi-tenancy related state, skip Rails CSRF token verification, or
|
44
|
+
# customise how Scimitar generates URLs:
|
45
|
+
#
|
46
|
+
# application_controller_mixin: Module.new do
|
47
|
+
# def self.included(base)
|
48
|
+
# base.class_eval do
|
49
|
+
#
|
50
|
+
# # Anything here is written just as you'd write it at the top of
|
51
|
+
# # one of your controller classes, but it gets included in all
|
52
|
+
# # Scimitar classes too.
|
53
|
+
#
|
54
|
+
# skip_before_action :verify_authenticity_token
|
55
|
+
# prepend_before_action :setup_some_kind_of_multi_tenancy_data
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# def scim_schemas_url(options)
|
60
|
+
# super(custom_param: 'value', **options)
|
61
|
+
# end
|
62
|
+
# end, # ...other configuration entries might follow...
|
63
|
+
|
64
|
+
# If you want to support username/password authentication:
|
65
|
+
#
|
66
|
+
# basic_authenticator: Proc.new do | username, password |
|
67
|
+
# # Check username/password and return 'true' if valid, else 'false'.
|
68
|
+
# end, # ...other configuration entries might follow...
|
69
|
+
#
|
70
|
+
# The 'username' and 'password' parameters come from Rails:
|
71
|
+
#
|
72
|
+
# https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
|
73
|
+
# https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic
|
74
|
+
|
75
|
+
# If you want to support HTTP bearer token (OAuth-style) authentication:
|
76
|
+
#
|
77
|
+
# token_authenticator: Proc.new do | token, options |
|
78
|
+
# # Check token and return 'true' if valid, else 'false'.
|
79
|
+
# end, # ...other configuration entries might follow...
|
80
|
+
#
|
81
|
+
# The 'token' and 'options' parameters come from Rails:
|
82
|
+
#
|
83
|
+
# https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
|
84
|
+
# https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token/ControllerMethods.html#method-i-authenticate_with_http_token
|
85
|
+
#
|
86
|
+
# Note that both basic and token authentication can be declared, with the
|
87
|
+
# parameters in the inbound HTTP request determining which is invoked.
|
88
|
+
|
89
|
+
# Scimitar rescues certain error cases and exceptions, in order to return a
|
90
|
+
# JSON response to the API caller. If you want exceptions to also be
|
91
|
+
# reported to a third party system such as sentry.io or raygun.com, you can
|
92
|
+
# configure a Proc to do so. It is passed a Ruby exception subclass object.
|
93
|
+
# For example, a minimal sentry.io reporter might do this:
|
94
|
+
#
|
95
|
+
# exception_reporter: Proc.new do | exception |
|
96
|
+
# Sentry.capture_exception(exception)
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# You will still need to configure your reporting system according to its
|
100
|
+
# documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
|
101
|
+
|
102
|
+
# Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
|
103
|
+
# used for e.g. e-mail addresses or phone numbers, as required by default.
|
104
|
+
# If you encounter a service which calls these with e.g. "null" value data,
|
105
|
+
# you can configure all values to be optional. You'll need to deal with
|
106
|
+
# whatever that means for you receiving system in your model code.
|
107
|
+
#
|
108
|
+
# optional_value_fields_required: false
|
109
|
+
})
|
110
|
+
|
111
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
Scimitar::Engine.routes.draw do
|
2
|
+
get 'ServiceProviderConfig', to: 'service_provider_configurations#show', as: :scim_service_provider_configuration
|
3
|
+
get 'ResourceTypes', to: 'resource_types#index', as: :scim_resource_types
|
4
|
+
get 'ResourceTypes/:name', to: 'resource_types#show', as: :scim_resource_type
|
5
|
+
get 'Schemas', to: 'schemas#index', as: :scim_schemas
|
6
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Scimitar
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace Scimitar
|
4
|
+
|
5
|
+
Mime::Type.register 'application/scim+json', :scim
|
6
|
+
|
7
|
+
ActionDispatch::Request.parameter_parsers[Mime::Type.lookup('application/scim+json').symbol] = lambda do |body|
|
8
|
+
JSON.parse(body)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.resources
|
12
|
+
default_resources + custom_resources
|
13
|
+
end
|
14
|
+
|
15
|
+
# Can be used to add a new resource type which is not provided by the gem.
|
16
|
+
# For example:
|
17
|
+
#
|
18
|
+
# module Scim
|
19
|
+
# module Resources
|
20
|
+
# class ShinyResource < Scimitar::Resources::Base
|
21
|
+
# set_schema Scim::Schema::Shiny
|
22
|
+
#
|
23
|
+
# def self.endpoint
|
24
|
+
# "/Shinies"
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Scimitar::Engine.add_custom_resource Scim::Resources::ShinyResource
|
31
|
+
#
|
32
|
+
def self.add_custom_resource(resource)
|
33
|
+
custom_resources << resource
|
34
|
+
end
|
35
|
+
|
36
|
+
# Resets the resource list to default. This is really only intended for use
|
37
|
+
# during testing, to avoid one test polluting another.
|
38
|
+
#
|
39
|
+
def self.reset_custom_resources
|
40
|
+
@custom_resources = []
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the list of custom resources, if any.
|
44
|
+
#
|
45
|
+
def self.custom_resources
|
46
|
+
@custom_resources ||= []
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the default resources added in this gem:
|
50
|
+
#
|
51
|
+
# * Scimitar::Resources::User
|
52
|
+
# * Scimitar::Resources::Group
|
53
|
+
#
|
54
|
+
def self.default_resources
|
55
|
+
[ Resources::User, Resources::Group ]
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.schemas
|
59
|
+
resources.map(&:schemas).flatten.uniq.map(&:new)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'active_support/hash_with_indifferent_access'
|
2
|
+
|
3
|
+
class Hash
|
4
|
+
|
5
|
+
# Converts this Hash to an instance of
|
6
|
+
# Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess, which is
|
7
|
+
# a subclass of ActiveSupport::HashWithIndifferentAccess with the addition of
|
8
|
+
# case-insensitive lookup.
|
9
|
+
#
|
10
|
+
# Note that this is more thorough than the ActiveSupport counterpart. It
|
11
|
+
# converts recursively, so that all Hashes to arbitrary depth, including any
|
12
|
+
# hashes inside Arrays, are converted. This is an expensive operation.
|
13
|
+
#
|
14
|
+
def with_indifferent_case_insensitive_access
|
15
|
+
self.class.deep_indifferent_case_insensitive_access(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Supports #with_indifferent_case_insensitive_access. Converts the given item
|
19
|
+
# to indifferent, case-insensitive access as a Hash; or converts Array items
|
20
|
+
# if given an Array; or returns the given object.
|
21
|
+
#
|
22
|
+
# Hashes and Arrays at all depths are duplicated as a result.
|
23
|
+
#
|
24
|
+
def self.deep_indifferent_case_insensitive_access(object)
|
25
|
+
if object.is_a?(Hash)
|
26
|
+
new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
|
27
|
+
object.each do | key, value |
|
28
|
+
new_hash[key] = deep_indifferent_case_insensitive_access(value)
|
29
|
+
end
|
30
|
+
new_hash
|
31
|
+
|
32
|
+
elsif object.is_a?(Array)
|
33
|
+
object.map do | array_entry |
|
34
|
+
deep_indifferent_case_insensitive_access(array_entry)
|
35
|
+
end
|
36
|
+
|
37
|
+
else
|
38
|
+
object
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Scimitar
|
45
|
+
module Support
|
46
|
+
|
47
|
+
# A subclass of ActiveSupport::HashWithIndifferentAccess where not only
|
48
|
+
# can Hash keys be queried as Symbols or Strings, but they are looked up
|
49
|
+
# in a case-insensitive fashion too.
|
50
|
+
#
|
51
|
+
# During enumeration, Hash keys will always be returned in whatever case
|
52
|
+
# they were originally set. Just as with
|
53
|
+
# ActiveSupport::HashWithIndifferentAccess, though, the type of the keys is
|
54
|
+
# always returned as a String, even if originally set as a Symbol - only
|
55
|
+
# the upper/lower case nature of the original key is preserved.
|
56
|
+
#
|
57
|
+
# If a key is written more than once with the same effective meaning in a
|
58
|
+
# to-string, to-downcase form, then whatever case was used *first* wins;
|
59
|
+
# e.g. if you did hash['User'] = 23, then hash['USER'] = 42, the result
|
60
|
+
# would be {"User" => 42}.
|
61
|
+
#
|
62
|
+
# It's important to remember that Hash#merge is shallow and replaces values
|
63
|
+
# found at existing keys in the target ("this") hash with values in the
|
64
|
+
# inbound Hash. If that new value that is itself a Hash, this *replaces*
|
65
|
+
# the value. For example:
|
66
|
+
#
|
67
|
+
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
68
|
+
# * Merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
|
69
|
+
#
|
70
|
+
# ...results in "this" target hash's key +Foo+ being addressed in the merge
|
71
|
+
# by inbound key +FOO+, so the case doesn't change. But the value for +Foo+
|
72
|
+
# is _replaced_ by the merging-in Hash completely:
|
73
|
+
#
|
74
|
+
# * Result: <tt>'Foo' => { 'BAR' => 24 }</tt>
|
75
|
+
#
|
76
|
+
# ...and of course we might've replaced with a totally different type, such
|
77
|
+
# as +true+:
|
78
|
+
#
|
79
|
+
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
80
|
+
# * Merge: <tt>'FOO' => true</tt>
|
81
|
+
# * Result: <tt>'Foo' => true</tt>
|
82
|
+
#
|
83
|
+
# If you're intending to merge nested Hashes, then use ActiveSupport's
|
84
|
+
# #deep_merge or an equivalent. This will have the expected outcome, where
|
85
|
+
# the hash with 'BAR' is _merged_ into the existing value and, therefore,
|
86
|
+
# the original 'Bar' key case is preserved:
|
87
|
+
#
|
88
|
+
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
89
|
+
# * Deep merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
|
90
|
+
# * Result: <tt>'Foo' => { 'Bar' => 24 }</tt>
|
91
|
+
#
|
92
|
+
class HashWithIndifferentCaseInsensitiveAccess < ActiveSupport::HashWithIndifferentAccess
|
93
|
+
def with_indifferent_case_insensitive_access
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def initialize(constructor = nil)
|
98
|
+
@scimitar_hash_with_indifferent_case_insensitive_access_key_map = {}
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
# It's vital that the attribute map is carried over when one of these
|
103
|
+
# objects is duplicated. Duplication of this ivar state does *not* happen
|
104
|
+
# when 'dup' is called on our superclass, so we have to do that manually.
|
105
|
+
#
|
106
|
+
def dup
|
107
|
+
duplicate = super
|
108
|
+
duplicate.instance_variable_set(
|
109
|
+
'@scimitar_hash_with_indifferent_case_insensitive_access_key_map',
|
110
|
+
@scimitar_hash_with_indifferent_case_insensitive_access_key_map
|
111
|
+
)
|
112
|
+
|
113
|
+
return duplicate
|
114
|
+
end
|
115
|
+
|
116
|
+
# Override the individual key writer.
|
117
|
+
#
|
118
|
+
def []=(key, value)
|
119
|
+
string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
120
|
+
indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
|
121
|
+
converted_value = convert_value(value, conversion: :assignment)
|
122
|
+
|
123
|
+
# Note '||=', as there might have been a prior use of the "same" key in
|
124
|
+
# a different case. The earliest one is preserved since the actual Hash
|
125
|
+
# underneath all this is already using that variant of the key.
|
126
|
+
#
|
127
|
+
key_for_writing = (
|
128
|
+
@scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] ||= string_key
|
129
|
+
)
|
130
|
+
|
131
|
+
regular_writer(key_for_writing, converted_value)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Override #merge to express it in terms of #merge! (also overridden), so
|
135
|
+
# that merged hashes can have their keys treated indifferently too.
|
136
|
+
#
|
137
|
+
def merge(*other_hashes, &block)
|
138
|
+
dup.merge!(*other_hashes, &block)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Modifies-self version of #merge, overriding Hash#merge!.
|
142
|
+
#
|
143
|
+
def merge!(*hashes_to_merge_to_self, &block)
|
144
|
+
if block_given?
|
145
|
+
hashes_to_merge_to_self.each do |hash_to_merge_to_self|
|
146
|
+
hash_to_merge_to_self.each_pair do |key, value|
|
147
|
+
value = block.call(key, self[key], value) if self.key?(key)
|
148
|
+
self[key] = value
|
149
|
+
end
|
150
|
+
end
|
151
|
+
else
|
152
|
+
hashes_to_merge_to_self.each do |hash_to_merge_to_self|
|
153
|
+
hash_to_merge_to_self.each_pair do |key, value|
|
154
|
+
self[key] = value
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
# =======================================================================
|
163
|
+
# PRIVATE INSTANCE METHODS
|
164
|
+
# =======================================================================
|
165
|
+
#
|
166
|
+
private
|
167
|
+
|
168
|
+
if Symbol.method_defined?(:name)
|
169
|
+
def scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
170
|
+
key.kind_of?(Symbol) ? key.name : key
|
171
|
+
end
|
172
|
+
else
|
173
|
+
def scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
174
|
+
key.kind_of?(Symbol) ? key.to_s : key
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def scimitar_hash_with_indifferent_case_insensitive_access_downcase(key)
|
179
|
+
key.kind_of?(String) ? key.downcase : key
|
180
|
+
end
|
181
|
+
|
182
|
+
def convert_key(key)
|
183
|
+
string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
184
|
+
indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
|
185
|
+
|
186
|
+
@scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] || string_key
|
187
|
+
end
|
188
|
+
|
189
|
+
def convert_value(value, conversion: nil)
|
190
|
+
if value.is_a?(Hash)
|
191
|
+
if conversion == :to_hash
|
192
|
+
value.to_hash
|
193
|
+
else
|
194
|
+
value.with_indifferent_case_insensitive_access
|
195
|
+
end
|
196
|
+
else
|
197
|
+
super
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def update_with_single_argument(other_hash, block)
|
202
|
+
if other_hash.is_a?(HashWithIndifferentCaseInsensitiveAccess)
|
203
|
+
regular_update(other_hash, &block)
|
204
|
+
else
|
205
|
+
other_hash.to_hash.each_pair do |key, value|
|
206
|
+
if block && key?(key)
|
207
|
+
value = block.call(self.convert_key(key), self[key], value)
|
208
|
+
end
|
209
|
+
self.[]=(key, value)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Scimitar
|
2
|
+
|
3
|
+
# Namespace containing various chunks of Scimitar support code that don't
|
4
|
+
# logically fit into other areas.
|
5
|
+
#
|
6
|
+
module Support
|
7
|
+
|
8
|
+
# A namespace that contains various stand-alone utility methods which act
|
9
|
+
# as helpers for other parts of the code base, without risking namespace
|
10
|
+
# pollution by e.g. being part of a module loaded into a client class.
|
11
|
+
#
|
12
|
+
module Utilities
|
13
|
+
|
14
|
+
# Takes an array of components that usually come from a dotted path such
|
15
|
+
# as <tt>foo.bar.baz</tt>, along with a value that is found at the end of
|
16
|
+
# that path, then converts it into a nested Hash with each level of the
|
17
|
+
# Hash corresponding to a step along the path.
|
18
|
+
#
|
19
|
+
# This was written to help with edge case SCIM uses where (most often, at
|
20
|
+
# least) inbound calls use a dotted notation where nested values are more
|
21
|
+
# commonly accepted; converting to nesting makes it easier for subsequent
|
22
|
+
# processing code, which needs only handle nested Hash data.
|
23
|
+
#
|
24
|
+
# As an example, passing:
|
25
|
+
#
|
26
|
+
# ['foo', 'bar', 'baz'], 'value'
|
27
|
+
#
|
28
|
+
# ...yields:
|
29
|
+
#
|
30
|
+
# {'foo' => {'bar' => {'baz' => 'value'}}}
|
31
|
+
#
|
32
|
+
# Parameters:
|
33
|
+
#
|
34
|
+
# +array+:: Array containing path components, usually acquired from a
|
35
|
+
# string with dot separators and a call to String#split.
|
36
|
+
#
|
37
|
+
# +value+:: The value found at the path indicated by +array+.
|
38
|
+
#
|
39
|
+
# If +array+ is empty, +value+ is returned directly, with no nesting
|
40
|
+
# Hash wrapping it.
|
41
|
+
#
|
42
|
+
def self.dot_path(array, value)
|
43
|
+
return value if array.empty?
|
44
|
+
|
45
|
+
{}.tap do | hash |
|
46
|
+
hash[array.shift()] = self.dot_path(array, value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Scimitar
|
2
|
+
|
3
|
+
# Gem version. If this changes, be sure to re-run "bundle install" or
|
4
|
+
# "bundle update".
|
5
|
+
#
|
6
|
+
VERSION = '1.0.0'
|
7
|
+
|
8
|
+
# Date for VERSION. If this changes, be sure to re-run "bundle install"
|
9
|
+
# or "bundle update".
|
10
|
+
#
|
11
|
+
DATE = '2024-04-23'
|
12
|
+
|
13
|
+
end
|
data/lib/scimitar.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'scimitar/version'
|
2
|
+
require 'scimitar/support/hash_with_indifferent_case_insensitive_access'
|
3
|
+
require 'scimitar/support/utilities'
|
4
|
+
require 'scimitar/engine'
|
5
|
+
|
6
|
+
module Scimitar
|
7
|
+
def self.service_provider_configuration=(custom_configuration)
|
8
|
+
if @service_provider_configuration.nil? || ! custom_configuration.uses_defaults
|
9
|
+
@service_provider_configuration = custom_configuration
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.service_provider_configuration(location:)
|
14
|
+
@service_provider_configuration ||= ServiceProviderConfiguration.new
|
15
|
+
@service_provider_configuration.meta.location = location
|
16
|
+
@service_provider_configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.engine_configuration=(custom_configuration)
|
20
|
+
if @engine_configuration.nil? || ! custom_configuration.uses_defaults
|
21
|
+
@engine_configuration = custom_configuration
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.engine_configuration
|
26
|
+
@engine_configuration ||= EngineConfiguration.new
|
27
|
+
@engine_configuration
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# For tests only - uses custom 'create' implementation which passes a block to
|
2
|
+
# Scimitar::ActiveRecordBackedResourcesController#create.
|
3
|
+
#
|
4
|
+
class CustomCreateMockUsersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
OVERRIDDEN_NAME = SecureRandom.uuid
|
7
|
+
|
8
|
+
def create
|
9
|
+
super do | resource |
|
10
|
+
resource.first_name = OVERRIDDEN_NAME
|
11
|
+
resource.save!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def storage_class
|
18
|
+
MockUser
|
19
|
+
end
|
20
|
+
|
21
|
+
def storage_scope
|
22
|
+
MockUser.all
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# For tests only - uses custom 'destroy' implementation which passes a block to
|
2
|
+
# Scimitar::ActiveRecordBackedResourcesController#destroy.
|
3
|
+
#
|
4
|
+
class CustomDestroyMockUsersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
NOT_REALLY_DELETED_USERNAME_INDICATOR = 'not really deleted'
|
7
|
+
|
8
|
+
def destroy
|
9
|
+
super do | resource |
|
10
|
+
resource.update!(username: NOT_REALLY_DELETED_USERNAME_INDICATOR)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def storage_class
|
17
|
+
MockUser
|
18
|
+
end
|
19
|
+
|
20
|
+
def storage_scope
|
21
|
+
MockUser.all
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# For tests only - uses custom 'replace' implementation which passes a block to
|
2
|
+
# Scimitar::ActiveRecordBackedResourcesController#create.
|
3
|
+
#
|
4
|
+
class CustomReplaceMockUsersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
OVERRIDDEN_NAME = SecureRandom.uuid
|
7
|
+
|
8
|
+
def replace
|
9
|
+
super do | resource |
|
10
|
+
resource.first_name = OVERRIDDEN_NAME
|
11
|
+
resource.save!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def storage_class
|
18
|
+
MockUser
|
19
|
+
end
|
20
|
+
|
21
|
+
def storage_scope
|
22
|
+
MockUser.all
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# For tests only - uses custom 'index' implementation which returns information
|
2
|
+
# from the Rails 'request' object in its response.
|
3
|
+
#
|
4
|
+
class CustomRequestVerifiersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
def index
|
7
|
+
render json: {
|
8
|
+
request: {
|
9
|
+
is_scim: request.format == :scim,
|
10
|
+
format: request.format.to_s,
|
11
|
+
content_type: request.headers['CONTENT_TYPE']
|
12
|
+
}
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def create
|
17
|
+
# Used for invalid JSON input tests
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def storage_class
|
23
|
+
MockUser
|
24
|
+
end
|
25
|
+
|
26
|
+
def storage_scope
|
27
|
+
MockUser.all
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# For tests only - uses custom 'save!' implementation which passes a block to
|
2
|
+
# Scimitar::ActiveRecordBackedResourcesController#save!.
|
3
|
+
#
|
4
|
+
class CustomSaveMockUsersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR = 'Custom save-block invoked'
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def save!(_record)
|
11
|
+
super do | record |
|
12
|
+
record.update!(username: CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def storage_class
|
17
|
+
MockUser
|
18
|
+
end
|
19
|
+
|
20
|
+
def storage_scope
|
21
|
+
MockUser.all
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# For tests only - uses custom 'update' implementation which passes a block to
|
2
|
+
# Scimitar::ActiveRecordBackedResourcesController#create.
|
3
|
+
#
|
4
|
+
class CustomUpdateMockUsersController < Scimitar::ActiveRecordBackedResourcesController
|
5
|
+
|
6
|
+
OVERRIDDEN_NAME = SecureRandom.uuid
|
7
|
+
|
8
|
+
def update
|
9
|
+
super do | resource |
|
10
|
+
resource.first_name = OVERRIDDEN_NAME
|
11
|
+
resource.save!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def storage_class
|
18
|
+
MockUser
|
19
|
+
end
|
20
|
+
|
21
|
+
def storage_scope
|
22
|
+
MockUser.all
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|