powerhome-scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +708 -0
  4. data/Rakefile +16 -0
  5. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +257 -0
  6. data/app/controllers/scimitar/application_controller.rb +157 -0
  7. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  8. data/app/controllers/scimitar/resources_controller.rb +203 -0
  9. data/app/controllers/scimitar/schemas_controller.rb +21 -0
  10. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  11. data/app/models/scimitar/authentication_error.rb +9 -0
  12. data/app/models/scimitar/authentication_scheme.rb +18 -0
  13. data/app/models/scimitar/bulk.rb +8 -0
  14. data/app/models/scimitar/complex_types/address.rb +12 -0
  15. data/app/models/scimitar/complex_types/base.rb +83 -0
  16. data/app/models/scimitar/complex_types/email.rb +12 -0
  17. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  18. data/app/models/scimitar/complex_types/ims.rb +12 -0
  19. data/app/models/scimitar/complex_types/name.rb +12 -0
  20. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  21. data/app/models/scimitar/complex_types/photo.rb +12 -0
  22. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  23. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  24. data/app/models/scimitar/complex_types/role.rb +12 -0
  25. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  26. data/app/models/scimitar/engine_configuration.rb +32 -0
  27. data/app/models/scimitar/error_response.rb +32 -0
  28. data/app/models/scimitar/errors.rb +14 -0
  29. data/app/models/scimitar/filter.rb +11 -0
  30. data/app/models/scimitar/filter_error.rb +22 -0
  31. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  32. data/app/models/scimitar/lists/count.rb +64 -0
  33. data/app/models/scimitar/lists/query_parser.rb +745 -0
  34. data/app/models/scimitar/meta.rb +7 -0
  35. data/app/models/scimitar/not_found_error.rb +10 -0
  36. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  37. data/app/models/scimitar/resource_type.rb +29 -0
  38. data/app/models/scimitar/resources/base.rb +190 -0
  39. data/app/models/scimitar/resources/group.rb +13 -0
  40. data/app/models/scimitar/resources/mixin.rb +1524 -0
  41. data/app/models/scimitar/resources/user.rb +13 -0
  42. data/app/models/scimitar/schema/address.rb +25 -0
  43. data/app/models/scimitar/schema/attribute.rb +132 -0
  44. data/app/models/scimitar/schema/base.rb +90 -0
  45. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  46. data/app/models/scimitar/schema/email.rb +10 -0
  47. data/app/models/scimitar/schema/entitlement.rb +10 -0
  48. data/app/models/scimitar/schema/group.rb +27 -0
  49. data/app/models/scimitar/schema/ims.rb +10 -0
  50. data/app/models/scimitar/schema/name.rb +20 -0
  51. data/app/models/scimitar/schema/phone_number.rb +10 -0
  52. data/app/models/scimitar/schema/photo.rb +10 -0
  53. data/app/models/scimitar/schema/reference_group.rb +23 -0
  54. data/app/models/scimitar/schema/reference_member.rb +21 -0
  55. data/app/models/scimitar/schema/role.rb +10 -0
  56. data/app/models/scimitar/schema/user.rb +52 -0
  57. data/app/models/scimitar/schema/vdtp.rb +18 -0
  58. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  59. data/app/models/scimitar/service_provider_configuration.rb +60 -0
  60. data/app/models/scimitar/supportable.rb +14 -0
  61. data/app/views/layouts/scimitar/application.html.erb +14 -0
  62. data/config/initializers/scimitar.rb +111 -0
  63. data/config/routes.rb +6 -0
  64. data/lib/scimitar/engine.rb +63 -0
  65. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +216 -0
  66. data/lib/scimitar/support/utilities.rb +51 -0
  67. data/lib/scimitar/version.rb +13 -0
  68. data/lib/scimitar.rb +29 -0
  69. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
  70. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  71. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
  72. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  73. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
  74. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
  75. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  76. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  77. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  78. data/spec/apps/dummy/app/models/mock_user.rb +132 -0
  79. data/spec/apps/dummy/config/application.rb +18 -0
  80. data/spec/apps/dummy/config/boot.rb +2 -0
  81. data/spec/apps/dummy/config/environment.rb +2 -0
  82. data/spec/apps/dummy/config/environments/test.rb +38 -0
  83. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  84. data/spec/apps/dummy/config/initializers/scimitar.rb +61 -0
  85. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  86. data/spec/apps/dummy/config/routes.rb +45 -0
  87. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +24 -0
  88. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  89. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +13 -0
  90. data/spec/apps/dummy/db/schema.rb +48 -0
  91. data/spec/controllers/scimitar/application_controller_spec.rb +296 -0
  92. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  93. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  94. data/spec/controllers/scimitar/schemas_controller_spec.rb +83 -0
  95. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  96. data/spec/models/scimitar/complex_types/address_spec.rb +18 -0
  97. data/spec/models/scimitar/complex_types/email_spec.rb +21 -0
  98. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  99. data/spec/models/scimitar/lists/query_parser_spec.rb +830 -0
  100. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  101. data/spec/models/scimitar/resources/base_spec.rb +485 -0
  102. data/spec/models/scimitar/resources/base_validation_spec.rb +86 -0
  103. data/spec/models/scimitar/resources/mixin_spec.rb +3562 -0
  104. data/spec/models/scimitar/resources/user_spec.rb +68 -0
  105. data/spec/models/scimitar/schema/attribute_spec.rb +99 -0
  106. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  107. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  108. data/spec/models/scimitar/schema/user_spec.rb +720 -0
  109. data/spec/requests/active_record_backed_resources_controller_spec.rb +1354 -0
  110. data/spec/requests/application_controller_spec.rb +61 -0
  111. data/spec/requests/controller_configuration_spec.rb +17 -0
  112. data/spec/requests/engine_spec.rb +45 -0
  113. data/spec/spec_helper.rb +101 -0
  114. data/spec/spec_helper_spec.rb +30 -0
  115. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +169 -0
  116. 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
@@ -0,0 +1,13 @@
1
+ class MockGroupsController < Scimitar::ActiveRecordBackedResourcesController
2
+
3
+ protected
4
+
5
+ def storage_class
6
+ MockGroup
7
+ end
8
+
9
+ def storage_scope
10
+ MockGroup.all
11
+ end
12
+
13
+ end
@@ -0,0 +1,13 @@
1
+ class MockUsersController < Scimitar::ActiveRecordBackedResourcesController
2
+
3
+ protected
4
+
5
+ def storage_class
6
+ MockUser
7
+ end
8
+
9
+ def storage_scope
10
+ MockUser.all
11
+ end
12
+
13
+ end