scimitar 1.5.0 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd0d129e52cd7cbe3fe52fec4c4033cd45f4d2071da5d495863b1f3f78685fd5
4
- data.tar.gz: 5531cdfc4786043513f9539c2fce43bfdebbec26a6e9964d93b13c6ea00710b0
3
+ metadata.gz: 6d57cfdaba9d48c6c193fb74baafc7c6e26c004a5a634460bc2f9ec94ad0440e
4
+ data.tar.gz: 8ff5ffbabe01c86822bd2c1dc85c535bf95b197cbc41920a995aa1801d239f32
5
5
  SHA512:
6
- metadata.gz: d1914b8fe68cdc6077369f22a03f7e688790f5b239738e18508e0aeaa68055ee1e197ebc4fe8b512903d07b926de64d64817b26c6ef9b9ace2594abc85f01e62
7
- data.tar.gz: de0d305d512d646f43e581b5116c2f316eaa2b9ddb0426f1b12522a668e2a7c811a5e9d6aee82e82fadb534b5e1a5cacca303701a7f7d1791de188d73b55740c
6
+ metadata.gz: 91f6cba011c909de21f7c391dbfc5b18a30b7ef9cf81d9f43842452fa021484f55623034be386af6242e9a1a82efa9ef9436d8b5c17248bf0b9384587fecb50b
7
+ data.tar.gz: e72e91fa8c3dd85df64b806dd3e6cb2b4d2460b33cb7d3c241bc64e8557adbccff9e3071fd18fab676ffdcd2a8c7f375512f85919e6ba10601021d127097bef0
@@ -99,10 +99,10 @@ module Scimitar
99
99
  def require_scim
100
100
  scim_mime_type = Mime::Type.lookup_by_extension(:scim).to_s
101
101
 
102
- if request.content_type.nil?
102
+ if request.media_type.nil? || request.media_type.empty?
103
103
  request.format = :scim
104
104
  request.headers['CONTENT_TYPE'] = scim_mime_type
105
- elsif request.content_type&.downcase == scim_mime_type
105
+ elsif request.media_type.downcase == scim_mime_type
106
106
  request.format = :scim
107
107
  elsif request.format == :scim
108
108
  request.headers['CONTENT_TYPE'] = scim_mime_type
@@ -7,13 +7,17 @@ module Scimitar
7
7
  class EngineConfiguration
8
8
  include ActiveModel::Model
9
9
 
10
- attr_accessor :basic_authenticator,
11
- :token_authenticator,
12
- :application_controller_mixin,
13
- :exception_reporter,
14
- :optional_value_fields_required
10
+ attr_accessor(
11
+ :uses_defaults,
12
+ :basic_authenticator,
13
+ :token_authenticator,
14
+ :application_controller_mixin,
15
+ :exception_reporter,
16
+ :optional_value_fields_required,
17
+ )
15
18
 
16
19
  def initialize(attributes = {})
20
+ @uses_defaults = attributes.empty?
17
21
 
18
22
  # Set defaults that may be overridden by the initializer.
19
23
  #
@@ -443,9 +443,30 @@ module Scimitar
443
443
  ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
444
444
  end
445
445
 
446
+ # Handle extension schema. Contributed by @bettysteger and
447
+ # @MorrisFreeman via:
448
+ #
449
+ # https://github.com/RIPAGlobal/scimitar/issues/48
450
+ # https://github.com/RIPAGlobal/scimitar/pull/49
451
+ #
452
+ # Note the ":" separating the schema ID (URN) from the attribute.
453
+ # The nature of JSON rendering / other payloads might lead you to
454
+ # expect a "." as with any complex types, but that's not the case;
455
+ # see https://tools.ietf.org/html/rfc7644#section-3.10, or
456
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
457
+ # particular, https://tools.ietf.org/html/rfc7644#page-35.
458
+ #
459
+ paths = []
460
+ self.class.scim_resource_type.extended_schemas.each do |schema|
461
+ path_str.downcase.split(schema.id.downcase + ':').drop(1).each do |path|
462
+ paths += [schema.id] + path.split('.')
463
+ end
464
+ end
465
+ paths = path_str.split('.') if paths.empty?
466
+
446
467
  self.from_patch_backend!(
447
468
  nature: nature,
448
- path: (path_str || '').split('.'),
469
+ path: paths,
449
470
  value: value,
450
471
  altering_hash: ci_scim_hash
451
472
  )
@@ -616,7 +637,19 @@ module Scimitar
616
637
  attrs_map_or_leaf_value.each do | scim_attribute, sub_attrs_map_or_leaf_value |
617
638
  next if scim_attribute&.to_s&.downcase == 'id' && path.empty?
618
639
 
619
- sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(scim_attribute.to_s)
640
+ # Handle extension schema. Contributed by @bettysteger and
641
+ # @MorrisFreeman via:
642
+ #
643
+ # https://github.com/RIPAGlobal/scimitar/issues/48
644
+ # https://github.com/RIPAGlobal/scimitar/pull/49
645
+ #
646
+ attribute_tree = []
647
+ resource_class.extended_schemas.each do |schema|
648
+ attribute_tree << schema.id and break if schema.scim_attributes.any? { |attribute| attribute.name == scim_attribute.to_s }
649
+ end
650
+ attribute_tree << scim_attribute.to_s
651
+
652
+ sub_scim_hash_or_leaf_value = scim_hash_or_leaf_value&.dig(*attribute_tree)
620
653
 
621
654
  self.from_scim_backend!(
622
655
  attrs_map_or_leaf_value: sub_attrs_map_or_leaf_value,
@@ -914,7 +947,7 @@ module Scimitar
914
947
  # which would imply "payload removes all users", there is the
915
948
  # clear intent to remove just one.
916
949
  #
917
- # https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
950
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
918
951
  # https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
919
952
  #
920
953
  # Since remove-all in the face of remove-one is destructive, we
@@ -9,11 +9,22 @@ module Scimitar
9
9
  class ServiceProviderConfiguration
10
10
  include ActiveModel::Model
11
11
 
12
- attr_accessor :patch, :bulk, :filter, :changePassword,
13
- :sort, :etag, :authenticationSchemes,
14
- :schemas, :meta
12
+ attr_accessor(
13
+ :uses_defaults,
14
+ :patch,
15
+ :bulk,
16
+ :filter,
17
+ :changePassword,
18
+ :sort,
19
+ :etag,
20
+ :authenticationSchemes,
21
+ :schemas,
22
+ :meta,
23
+ )
15
24
 
16
25
  def initialize(attributes = {})
26
+ @uses_defaults = attributes.empty?
27
+
17
28
  defaults = {
18
29
  bulk: Supportable.unsupported,
19
30
  changePassword: Supportable.unsupported,
@@ -2,101 +2,105 @@
2
2
  #
3
3
  # For supporting information and rationale, please see README.md.
4
4
 
5
- # =============================================================================
6
- # SERVICE PROVIDER CONFIGURATION
7
- # =============================================================================
8
- #
9
- # This is a Ruby abstraction over a SCIM entity that declares the capabilities
10
- # supported by a particular implementation.
11
- #
12
- # Typically this is used to declare parts of the standard unsupported, if you
13
- # don't need them and don't want to provide subclass support.
14
- #
15
- Scimitar.service_provider_configuration = Scimitar::ServiceProviderConfiguration.new({
5
+ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
16
6
 
17
- # See https://tools.ietf.org/html/rfc7643#section-8.5 for properties.
7
+ # ===========================================================================
8
+ # SERVICE PROVIDER CONFIGURATION
9
+ # ===========================================================================
18
10
  #
19
- # See Gem source file 'app/models/scimitar/service_provider_configuration.rb'
20
- # for defaults. Define Hash keys here that override defaults; e.g. to declare
21
- # that filters are not supported so that calling clients shouldn't use them:
11
+ # This is a Ruby abstraction over a SCIM entity that declares the
12
+ # capabilities supported by a particular implementation.
22
13
  #
23
- # filter: Scimitar::Supported.unsupported
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({
24
18
 
25
- })
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
26
27
 
27
- # =============================================================================
28
- # ENGINE CONFIGURATION
29
- # =============================================================================
30
- #
31
- # This is where you provide callbacks for things like authorisation or mixins
32
- # that get included into all Scimitar-derived controllers (for things like
33
- # before-actions that apply to all Scimitar controller-based routes).
34
- #
35
- Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
28
+ })
36
29
 
37
- # If you have filters you want to run for any Scimitar action/route, you can
38
- # define them here. For example, you might use a before-action to set up some
39
- # multi-tenancy related state, or skip Rails CSRF token verification/
40
- #
41
- # For example:
42
- #
43
- # application_controller_mixin: Module.new do
44
- # def self.included(base)
45
- # base.class_eval do
30
+ # ===========================================================================
31
+ # ENGINE CONFIGURATION
32
+ # ===========================================================================
46
33
  #
47
- # # Anything here is written just as you'd write it at the top of
48
- # # one of your controller classes, but it gets included in all
49
- # # Scimitar classes too.
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).
50
37
  #
51
- # skip_before_action :verify_authenticity_token
52
- # prepend_before_action :setup_some_kind_of_multi_tenancy_data
53
- # end
54
- # end
55
- # end, # ...other configuration entries might follow...
38
+ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
56
39
 
57
- # If you want to support username/password authentication:
58
- #
59
- # basic_authenticator: Proc.new do | username, password |
60
- # # Check username/password and return 'true' if valid, else 'false'.
61
- # end, # ...other configuration entries might follow...
62
- #
63
- # The 'username' and 'password' parameters come from Rails:
64
- #
65
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
66
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic
40
+ # If you have filters you want to run for any Scimitar action/route, you
41
+ # can define them here. For example, you might use a before-action to set
42
+ # up some multi-tenancy related state, or skip Rails CSRF token
43
+ # verification. For example:
44
+ #
45
+ # application_controller_mixin: Module.new do
46
+ # def self.included(base)
47
+ # base.class_eval do
48
+ #
49
+ # # Anything here is written just as you'd write it at the top of
50
+ # # one of your controller classes, but it gets included in all
51
+ # # Scimitar classes too.
52
+ #
53
+ # skip_before_action :verify_authenticity_token
54
+ # prepend_before_action :setup_some_kind_of_multi_tenancy_data
55
+ # end
56
+ # end
57
+ # end, # ...other configuration entries might follow...
67
58
 
68
- # If you want to support HTTP bearer token (OAuth-style) authentication:
69
- #
70
- # token_authenticator: Proc.new do | token, options |
71
- # # Check token and return 'true' if valid, else 'false'.
72
- # end, # ...other configuration entries might follow...
73
- #
74
- # The 'token' and 'options' parameters come from Rails:
75
- #
76
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
77
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token/ControllerMethods.html#method-i-authenticate_with_http_token
78
- #
79
- # Note that both basic and token authentication can be declared, with the
80
- # parameters in the inbound HTTP request determining which is invoked.
59
+ # If you want to support username/password authentication:
60
+ #
61
+ # basic_authenticator: Proc.new do | username, password |
62
+ # # Check username/password and return 'true' if valid, else 'false'.
63
+ # end, # ...other configuration entries might follow...
64
+ #
65
+ # The 'username' and 'password' parameters come from Rails:
66
+ #
67
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
68
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic
81
69
 
82
- # Scimitar rescues certain error cases and exceptions, in order to return a
83
- # JSON response to the API caller. If you want exceptions to also be
84
- # reported to a third party system such as sentry.io or raygun.com, you can
85
- # configure a Proc to do so. It is passed a Ruby exception subclass object.
86
- # For example, a minimal sentry.io reporter might do this:
87
- #
88
- # exception_reporter: Proc.new do | exception |
89
- # Sentry.capture_exception(exception)
90
- # end
91
- #
92
- # You will still need to configure your reporting system according to its
93
- # documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
70
+ # If you want to support HTTP bearer token (OAuth-style) authentication:
71
+ #
72
+ # token_authenticator: Proc.new do | token, options |
73
+ # # Check token and return 'true' if valid, else 'false'.
74
+ # end, # ...other configuration entries might follow...
75
+ #
76
+ # The 'token' and 'options' parameters come from Rails:
77
+ #
78
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
79
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token/ControllerMethods.html#method-i-authenticate_with_http_token
80
+ #
81
+ # Note that both basic and token authentication can be declared, with the
82
+ # parameters in the inbound HTTP request determining which is invoked.
94
83
 
95
- # Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
96
- # used for e.g. e-mail addresses or phone numbers, as required by default.
97
- # If you encounter a service which calls these with e.g. "null" value data,
98
- # you can configure all values to be optional. You'll need to deal with
99
- # whatever that means for you receiving system in your model code.
100
- #
101
- # optional_value_fields_required: false
102
- })
84
+ # Scimitar rescues certain error cases and exceptions, in order to return a
85
+ # JSON response to the API caller. If you want exceptions to also be
86
+ # reported to a third party system such as sentry.io or raygun.com, you can
87
+ # configure a Proc to do so. It is passed a Ruby exception subclass object.
88
+ # For example, a minimal sentry.io reporter might do this:
89
+ #
90
+ # exception_reporter: Proc.new do | exception |
91
+ # Sentry.capture_exception(exception)
92
+ # end
93
+ #
94
+ # You will still need to configure your reporting system according to its
95
+ # documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
96
+
97
+ # Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
98
+ # used for e.g. e-mail addresses or phone numbers, as required by default.
99
+ # If you encounter a service which calls these with e.g. "null" value data,
100
+ # you can configure all values to be optional. You'll need to deal with
101
+ # whatever that means for you receiving system in your model code.
102
+ #
103
+ # optional_value_fields_required: false
104
+ })
105
+
106
+ end
@@ -3,11 +3,11 @@ module Scimitar
3
3
  # Gem version. If this changes, be sure to re-run "bundle install" or
4
4
  # "bundle update".
5
5
  #
6
- VERSION = '1.5.0'
6
+ VERSION = '1.5.3'
7
7
 
8
8
  # Date for VERSION. If this changes, be sure to re-run "bundle install"
9
9
  # or "bundle update".
10
10
  #
11
- DATE = '2023-01-27'
11
+ DATE = '2023-09-16'
12
12
 
13
13
  end
data/lib/scimitar.rb CHANGED
@@ -4,7 +4,9 @@ require 'scimitar/engine'
4
4
 
5
5
  module Scimitar
6
6
  def self.service_provider_configuration=(custom_configuration)
7
- @service_provider_configuration = custom_configuration
7
+ if @service_provider_configuration.nil? || ! custom_configuration.uses_defaults
8
+ @service_provider_configuration = custom_configuration
9
+ end
8
10
  end
9
11
 
10
12
  def self.service_provider_configuration(location:)
@@ -14,11 +16,25 @@ module Scimitar
14
16
  end
15
17
 
16
18
  def self.engine_configuration=(custom_configuration)
17
- @engine_configuration = custom_configuration
19
+ if @engine_configuration.nil? || ! custom_configuration.uses_defaults
20
+ @engine_configuration = custom_configuration
21
+ end
18
22
  end
19
23
 
20
24
  def self.engine_configuration
21
25
  @engine_configuration ||= EngineConfiguration.new
22
26
  @engine_configuration
23
27
  end
28
+
29
+ # Set in a "Rails.application.config.to_prepare" block by Scimitar itself to
30
+ # establish default values. Older Scimitar client applications might not use
31
+ # that wrapper; we don't want to overwrite settings they configured, but we
32
+ # *do* want to let them overwrite the defaults. Thus, '||=" is used here but
33
+ # not in ::service_provider_configuration=.
34
+ #
35
+ # Client applications should not call this method themselves.
36
+ #
37
+ def self.default_service_provider_configuration(default_configuration)
38
+ @service_provider_configuration ||= custom_configuration
39
+ end
24
40
  end
@@ -13,6 +13,8 @@ class MockUser < ActiveRecord::Base
13
13
  work_email_address
14
14
  home_email_address
15
15
  work_phone_number
16
+ organization
17
+ department
16
18
  }
17
19
 
18
20
  has_and_belongs_to_many :mock_groups
@@ -82,7 +84,13 @@ class MockUser < ActiveRecord::Base
82
84
  }
83
85
  }
84
86
  ],
85
- active: :is_active
87
+ active: :is_active,
88
+
89
+ # Custom extension schema - see configuration in
90
+ # "spec/apps/dummy/config/initializers/scimitar.rb".
91
+ #
92
+ organization: :organization,
93
+ department: :department
86
94
  }
87
95
  end
88
96
 
@@ -1,5 +1,22 @@
1
1
  # Test app configuration.
2
2
  #
3
+ # Note that as a result of https://github.com/RIPAGlobal/scimitar/issues/48,
4
+ # tests include a custom extension of the core User schema. A shortcoming of
5
+ # some of the code from which Scimitar was originally built is that those
6
+ # extensions are done with class-level ivars, so it is largely impossible (or
7
+ # at least, impractical in tests) to avoid polluting the core class itself
8
+ # with the extension.
9
+ #
10
+ # All related schema tests are written with this in mind.
11
+ #
12
+ # Further, https://github.com/RIPAGlobal/scimitar/pull/54 fixed warning
13
+ # messages in a way that worked on Rails 6+ but, for V1 Scimitar, it would
14
+ # break existing working setups that didn't use the +to_prepare+ wrapper. Their
15
+ # application configuration would be written *first* but then *overwritten* by
16
+ # the default +to_prepare+ block in Scimitar itself, since that runs later. The
17
+ # file below does *not* use +to_prepare+ in order to test the workaround that
18
+ # was produced; it should work on all Ruby versions as-is.
19
+ #
3
20
  Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
4
21
 
5
22
  application_controller_mixin: Module.new do
@@ -12,3 +29,31 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
12
29
  end
13
30
 
14
31
  })
32
+
33
+ module ScimSchemaExtensions
34
+ module User
35
+ class Enterprise < Scimitar::Schema::Base
36
+ def initialize(options = {})
37
+ super(
38
+ name: 'ExtendedUser',
39
+ description: 'Enterprise extension for a User',
40
+ id: self.class.id,
41
+ scim_attributes: self.class.scim_attributes
42
+ )
43
+ end
44
+
45
+ def self.id
46
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
47
+ end
48
+
49
+ def self.scim_attributes
50
+ [
51
+ Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
52
+ Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
53
+ ]
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Enterprise
@@ -3,6 +3,8 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
3
3
  create_table :mock_users, id: :uuid, primary_key: :primary_key do |t|
4
4
  t.timestamps
5
5
 
6
+ # Support part of the core schema
7
+ #
6
8
  t.text :scim_uid
7
9
  t.text :username
8
10
  t.text :first_name
@@ -10,6 +12,12 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
10
12
  t.text :work_email_address
11
13
  t.text :home_email_address
12
14
  t.text :work_phone_number
15
+
16
+ # Support the custom extension schema - see configuration in
17
+ # "spec/apps/dummy/config/initializers/scimitar.rb".
18
+ #
19
+ t.text :organization
20
+ t.text :department
13
21
  end
14
22
  end
15
23
  end
@@ -39,6 +39,8 @@ ActiveRecord::Schema.define(version: 2021_03_08_044214) do
39
39
  t.text "work_email_address"
40
40
  t.text "home_email_address"
41
41
  t.text "work_phone_number"
42
+ t.text "organization"
43
+ t.text "department"
42
44
  end
43
45
 
44
46
  add_foreign_key "mock_groups_users", "mock_groups"
@@ -14,9 +14,9 @@ RSpec.describe Scimitar::SchemasController do
14
14
  get :index, params: { format: :scim }
15
15
  expect(response).to be_ok
16
16
  parsed_body = JSON.parse(response.body)
17
- expect(parsed_body.length).to eql(2)
17
+ expect(parsed_body.length).to eql(3)
18
18
  schema_names = parsed_body.map {|schema| schema['name']}
19
- expect(schema_names).to match_array(['User', 'Group'])
19
+ expect(schema_names).to match_array(['User', 'ExtendedUser', 'Group'])
20
20
  end
21
21
 
22
22
  it 'returns only the User schema when its id is provided' do
@@ -250,90 +250,185 @@ RSpec.describe Scimitar::Resources::Base do
250
250
  end # "context 'dynamic setters based on schema' do"
251
251
 
252
252
  context 'schema extension' do
253
- ThirdCustomSchema = Class.new(Scimitar::Schema::Base) do
254
- def self.id
255
- 'custom-id'
256
- end
253
+ context 'of custom schema' do
254
+ ThirdCustomSchema = Class.new(Scimitar::Schema::Base) do
255
+ def self.id
256
+ 'custom-id'
257
+ end
257
258
 
258
- def self.scim_attributes
259
- [ Scimitar::Schema::Attribute.new(name: 'name', type: 'string') ]
259
+ def self.scim_attributes
260
+ [ Scimitar::Schema::Attribute.new(name: 'name', type: 'string') ]
261
+ end
260
262
  end
261
- end
262
263
 
263
- ExtensionSchema = Class.new(Scimitar::Schema::Base) do
264
- def self.id
265
- 'extension-id'
266
- end
264
+ ExtensionSchema = Class.new(Scimitar::Schema::Base) do
265
+ def self.id
266
+ 'extension-id'
267
+ end
267
268
 
268
- def self.scim_attributes
269
- [ Scimitar::Schema::Attribute.new(name: 'relationship', type: 'string', required: true) ]
269
+ def self.scim_attributes
270
+ [ Scimitar::Schema::Attribute.new(name: 'relationship', type: 'string', required: true) ]
271
+ end
270
272
  end
271
- end
272
273
 
273
- let(:resource_class) {
274
- Class.new(Scimitar::Resources::Base) do
275
- set_schema ThirdCustomSchema
276
- extend_schema ExtensionSchema
274
+ let(:resource_class) {
275
+ Class.new(Scimitar::Resources::Base) do
276
+ set_schema ThirdCustomSchema
277
+ extend_schema ExtensionSchema
277
278
 
278
- def self.endpoint
279
- '/gaga'
279
+ def self.endpoint
280
+ '/gaga'
281
+ end
282
+
283
+ def self.resource_type_id
284
+ 'CustomResource'
285
+ end
280
286
  end
287
+ }
281
288
 
282
- def self.resource_type_id
283
- 'CustomResource'
289
+ context '#initialize' do
290
+ it 'allows setting extension attributes' do
291
+ resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
292
+ expect(resource.relationship).to eql('GAGA')
284
293
  end
285
- end
286
- }
294
+ end # "context '#initialize' do"
295
+
296
+ context '#as_json' do
297
+ it 'namespaces the extension attributes' do
298
+ resource = resource_class.new(relationship: 'GAGA')
299
+ hash = resource.as_json
300
+ expect(hash["schemas"]).to eql(['custom-id', 'extension-id'])
301
+ expect(hash["extension-id"]).to eql("relationship" => 'GAGA')
302
+ end
303
+ end # "context '#as_json' do"
287
304
 
288
- context '#initialize' do
289
- it 'allows setting extension attributes' do
290
- resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
291
- expect(resource.relationship).to eql('GAGA')
292
- end
293
- end # "context '#initialize' do"
305
+ context '.resource_type' do
306
+ it 'appends the extension schemas' do
307
+ resource_type = resource_class.resource_type('http://gaga')
308
+ expect(resource_type.meta.location).to eql('http://gaga')
309
+ expect(resource_type.schemaExtensions.count).to eql(1)
310
+ end
294
311
 
295
- context '#as_json' do
296
- it 'namespaces the extension attributes' do
297
- resource = resource_class.new(relationship: 'GAGA')
298
- hash = resource.as_json
299
- expect(hash["schemas"]).to eql(['custom-id', 'extension-id'])
300
- expect(hash["extension-id"]).to eql("relationship" => 'GAGA')
301
- end
302
- end # "context '#as_json' do"
312
+ context 'validation' do
313
+ it 'validates into custom schema' do
314
+ resource = resource_class.new('extension-id' => {})
315
+ expect(resource.valid?).to eql(false)
303
316
 
304
- context '.resource_type' do
305
- it 'appends the extension schemas' do
306
- resource_type = resource_class.resource_type('http://gaga')
307
- expect(resource_type.meta.location).to eql('http://gaga')
308
- expect(resource_type.schemaExtensions.count).to eql(1)
309
- end
317
+ resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
318
+ expect(resource.relationship).to eql('GAGA')
319
+ expect(resource.valid?).to eql(true)
320
+ end
321
+ end # context 'validation'
322
+ end # "context '.resource_type' do"
310
323
 
311
- context 'validation' do
312
- it 'validates into custom schema' do
313
- resource = resource_class.new('extension-id' => {})
314
- expect(resource.valid?).to eql(false)
324
+ context '.find_attribute' do
325
+ it 'finds in first schema' do
326
+ found = resource_class().find_attribute('name') # Defined in ThirdCustomSchema
327
+ expect(found).to be_present
328
+ expect(found.name).to eql('name')
329
+ expect(found.type).to eql('string')
330
+ end
315
331
 
316
- resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
317
- expect(resource.relationship).to eql('GAGA')
318
- expect(resource.valid?).to eql(true)
332
+ it 'finds across schemas' do
333
+ found = resource_class().find_attribute('relationship') # Defined in ExtensionSchema
334
+ expect(found).to be_present
335
+ expect(found.name).to eql('relationship')
336
+ expect(found.type).to eql('string')
319
337
  end
320
- end # context 'validation'
321
- end # "context '.resource_type' do"
338
+ end # "context '.find_attribute' do"
339
+ end # "context 'of custom schema' do"
322
340
 
323
- context '.find_attribute' do
324
- it 'finds in first schema' do
325
- found = resource_class().find_attribute('name') # Defined in ThirdCustomSchema
326
- expect(found).to be_present
327
- expect(found.name).to eql('name')
328
- expect(found.type).to eql('string')
329
- end
341
+ context 'of core schema' do
342
+ EnterpriseExtensionSchema = Class.new(Scimitar::Schema::Base) do
343
+ def self.id
344
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
345
+ end
330
346
 
331
- it 'finds across schemas' do
332
- found = resource_class().find_attribute('relationship') # Defined in ExtensionSchema
333
- expect(found).to be_present
334
- expect(found.name).to eql('relationship')
335
- expect(found.type).to eql('string')
347
+ def self.scim_attributes
348
+ [
349
+ Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
350
+ Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
351
+ ]
352
+ end
336
353
  end
337
- end # "context '.find_attribute' do"
354
+
355
+ let(:resource_class) {
356
+ Class.new(Scimitar::Resources::Base) do
357
+ set_schema Scimitar::Schema::User
358
+ extend_schema EnterpriseExtensionSchema
359
+
360
+ def self.endpoint
361
+ '/Users'
362
+ end
363
+
364
+ def self.resource_type_id
365
+ 'User'
366
+ end
367
+ end
368
+ }
369
+
370
+ context '#initialize' do
371
+ it 'allows setting extension attributes' do
372
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {organization: 'SOMEORG', department: 'SOMEDPT'})
373
+
374
+ expect(resource.organization).to eql('SOMEORG')
375
+ expect(resource.department ).to eql('SOMEDPT')
376
+ end
377
+ end # "context '#initialize' do"
378
+
379
+ context '#as_json' do
380
+ it 'namespaces the extension attributes' do
381
+ resource = resource_class.new(organization: 'SOMEORG', department: 'SOMEDPT')
382
+ hash = resource.as_json
383
+
384
+ expect(hash['schemas']).to eql(['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'])
385
+ expect(hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']).to eql('organization' => 'SOMEORG', 'department' => 'SOMEDPT')
386
+ end
387
+ end # "context '#as_json' do"
388
+
389
+ context '.resource_type' do
390
+ it 'appends the extension schemas' do
391
+ resource_type = resource_class.resource_type('http://example.com')
392
+ expect(resource_type.meta.location).to eql('http://example.com')
393
+ expect(resource_type.schemaExtensions.count).to eql(1)
394
+ end
395
+
396
+ context 'validation' do
397
+ it 'validates into custom schema' do
398
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {})
399
+ expect(resource.valid?).to eql(false)
400
+
401
+ resource = resource_class.new(
402
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
403
+ userName: 'SOMEUSR',
404
+ organization: 'SOMEORG',
405
+ department: 'SOMEDPT'
406
+ }
407
+ )
408
+
409
+ expect(resource.organization).to eql('SOMEORG')
410
+ expect(resource.department ).to eql('SOMEDPT')
411
+ expect(resource.valid? ).to eql(true)
412
+ end
413
+ end # context 'validation'
414
+ end # "context '.resource_type' do"
415
+
416
+ context '.find_attribute' do
417
+ it 'finds in first schema' do
418
+ found = resource_class().find_attribute('userName') # Defined in Scimitar::Schema::User
419
+
420
+ expect(found).to be_present
421
+ expect(found.name).to eql('userName')
422
+ expect(found.type).to eql('string')
423
+ end
424
+
425
+ it 'finds across schemas' do
426
+ found = resource_class().find_attribute('organization') # Defined in EnterpriseExtensionSchema
427
+ expect(found).to be_present
428
+ expect(found.name).to eql('organization')
429
+ expect(found.type).to eql('string')
430
+ end
431
+ end # "context '.find_attribute' do"
432
+ end # "context 'of core schema' do"
338
433
  end # "context 'schema extension' do"
339
434
  end
@@ -172,6 +172,7 @@ RSpec.describe Scimitar::Resources::Mixin do
172
172
  instance.work_email_address = 'foo.bar@test.com'
173
173
  instance.home_email_address = nil
174
174
  instance.work_phone_number = '+642201234567'
175
+ instance.organization = 'SOMEORG'
175
176
 
176
177
  g1 = MockGroup.create!(display_name: 'Group 1')
177
178
  g2 = MockGroup.create!(display_name: 'Group 2')
@@ -194,7 +195,12 @@ RSpec.describe Scimitar::Resources::Mixin do
194
195
  'externalId' => 'AA02984',
195
196
  'groups' => [{'display'=>g1.display_name, 'value'=>g1.id.to_s}, {'display'=>g3.display_name, 'value'=>g3.id.to_s}],
196
197
  'meta' => {'location'=>"https://test.com/mock_users/#{uuid}", 'resourceType'=>'User'},
197
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
198
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
199
+
200
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
201
+ 'organization' => 'SOMEORG',
202
+ 'department' => nil
203
+ }
198
204
  })
199
205
  end
200
206
  end # "context 'with a UUID, renamed primary key column' do"
@@ -318,7 +324,9 @@ RSpec.describe Scimitar::Resources::Mixin do
318
324
  ],
319
325
 
320
326
  'meta' => {'location'=>'https://test.com/static_map_test', 'resourceType'=>'User'},
321
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
327
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
328
+
329
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {}
322
330
  })
323
331
  end
324
332
  end # "context 'using static mappings' do"
@@ -345,7 +353,9 @@ RSpec.describe Scimitar::Resources::Mixin do
345
353
  ],
346
354
 
347
355
  'meta' => {'location'=>'https://test.com/dynamic_map_test', 'resourceType'=>'User'},
348
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
356
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
357
+
358
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {}
349
359
  })
350
360
  end
351
361
  end # "context 'using dynamic lists' do"
@@ -402,7 +412,12 @@ RSpec.describe Scimitar::Resources::Mixin do
402
412
  'id' => '42', # Note, String
403
413
  'externalId' => 'AA02984',
404
414
  'meta' => {'location' => 'https://test.com/mock_users/42', 'resourceType' => 'User'},
405
- 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User']
415
+ 'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
416
+
417
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => {
418
+ 'organization' => 'SOMEORG',
419
+ 'DEPARTMENT' => 'SOMEDPT'
420
+ }
406
421
  }
407
422
 
408
423
  hash = spec_helper_hupcase(hash) if force_upper_case
@@ -418,6 +433,8 @@ RSpec.describe Scimitar::Resources::Mixin do
418
433
  expect(instance.work_email_address).to eql('foo.bar@test.com')
419
434
  expect(instance.home_email_address).to be_nil
420
435
  expect(instance.work_phone_number ).to eql('+642201234567')
436
+ expect(instance.organization ).to eql('SOMEORG')
437
+ expect(instance.department ).to eql('SOMEDPT')
421
438
  end
422
439
 
423
440
  it 'honouring read-write lists' do
@@ -704,6 +721,21 @@ RSpec.describe Scimitar::Resources::Mixin do
704
721
  expect(scim_hash['name']['familyName']).to eql('Bar')
705
722
  end
706
723
 
724
+ it 'with schema extensions: overwrites' do
725
+ path = [ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', 'organization' ]
726
+ scim_hash = { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => { 'organization' => 'SOMEORG' } }.with_indifferent_case_insensitive_access()
727
+
728
+ @instance.send(
729
+ :from_patch_backend!,
730
+ nature: 'add',
731
+ path: path,
732
+ value: 'OTHERORG',
733
+ altering_hash: scim_hash
734
+ )
735
+
736
+ expect(scim_hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization' ]).to eql('OTHERORG')
737
+ end
738
+
707
739
  # For 'add', filter at end-of-path is nonsensical and not
708
740
  # supported by spec or Scimitar; we only test mid-path filters.
709
741
  #
@@ -892,6 +924,21 @@ RSpec.describe Scimitar::Resources::Mixin do
892
924
  expect(scim_hash['name']['givenName']).to eql('Baz')
893
925
  end
894
926
 
927
+ it 'with schema extensions: adds' do
928
+ path = [ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', 'organization' ]
929
+ scim_hash = {}.with_indifferent_case_insensitive_access()
930
+
931
+ @instance.send(
932
+ :from_patch_backend!,
933
+ nature: 'add',
934
+ path: path,
935
+ value: 'SOMEORG',
936
+ altering_hash: scim_hash
937
+ )
938
+
939
+ expect(scim_hash['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization' ]).to eql('SOMEORG')
940
+ end
941
+
895
942
  context 'with filter mid-path: adds' do
896
943
  it 'by string match' do
897
944
  path = [ 'emails[type eq "work"]', 'value' ]
@@ -1233,7 +1280,7 @@ RSpec.describe Scimitar::Resources::Mixin do
1233
1280
 
1234
1281
  # What we expect:
1235
1282
  #
1236
- # https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
1283
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
1237
1284
  # https://docs.snowflake.com/en/user-guide/scim-intro.html#patch-scim-v2-groups-id
1238
1285
  #
1239
1286
  # ...vs accounting for the unusual payloads we sometimes get,
@@ -2680,6 +2727,38 @@ RSpec.describe Scimitar::Resources::Mixin do
2680
2727
  expect(@instance.first_name).to eql('Baz')
2681
2728
  end
2682
2729
 
2730
+ # Note odd ":" separating schema ID from first attribute, although
2731
+ # the nature of JSON rendering / other payloads might lead you to
2732
+ # expect a "." as with any other path component.
2733
+ #
2734
+ # Note the ":" separating the schema ID (URN) from the attribute.
2735
+ # The nature of JSON rendering / other payloads might lead you to
2736
+ # expect a "." as with any complex types, but that's not the case;
2737
+ # see https://tools.ietf.org/html/rfc7644#section-3.10, or
2738
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
2739
+ # particular, https://tools.ietf.org/html/rfc7644#page-35.
2740
+ #
2741
+ it 'which updates attributes defined by extension schema' do
2742
+ @instance.update!(department: 'SOMEDPT')
2743
+
2744
+ path = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department'
2745
+ path = path.upcase if force_upper_case
2746
+
2747
+ patch = {
2748
+ 'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
2749
+ 'Operations' => [
2750
+ {
2751
+ 'op' => 'replace',
2752
+ 'path' => path,
2753
+ 'value' => 'OTHERDPT'
2754
+ }
2755
+ ]
2756
+ }
2757
+
2758
+ @instance.from_scim_patch!(patch_hash: patch)
2759
+ expect(@instance.department).to eql('OTHERDPT')
2760
+ end
2761
+
2683
2762
  it 'which updates with filter match' do
2684
2763
  @instance.update!(work_email_address: 'work@test.com', home_email_address: 'home@test.com')
2685
2764
 
@@ -762,7 +762,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
762
762
  end
763
763
  end
764
764
 
765
- # https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2.2
765
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
766
766
  #
767
767
  context 'and using an RFC-compliant payload' do
768
768
  let(:removed_user) { @u2 }
@@ -39,6 +39,16 @@ RSpec.describe Scimitar::ApplicationController do
39
39
  expect(parsed_body['request']['content_type']).to eql('application/scim+json')
40
40
  end
41
41
 
42
+ it 'translates Content-Type with charset to Rails request format' do
43
+ get '/CustomRequestVerifiers', headers: { 'CONTENT_TYPE' => 'application/scim+json; charset=utf-8' }
44
+
45
+ expect(response).to have_http_status(:ok)
46
+ parsed_body = JSON.parse(response.body)
47
+ expect(parsed_body['request']['is_scim' ]).to eql(true)
48
+ expect(parsed_body['request']['format' ]).to eql('application/scim+json')
49
+ expect(parsed_body['request']['content_type']).to eql('application/scim+json; charset=utf-8')
50
+ end
51
+
42
52
  it 'translates Rails request format to header' do
43
53
  get '/CustomRequestVerifiers', params: { format: :scim }
44
54
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scimitar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - RIPA Global
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-01-27 00:00:00.000000000 Z
12
+ date: 2023-09-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails