scimitar 1.8.2 → 1.10.0

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -20
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
  4. data/app/controllers/scimitar/resource_types_controller.rb +0 -2
  5. data/app/controllers/scimitar/resources_controller.rb +0 -2
  6. data/app/controllers/scimitar/schemas_controller.rb +361 -3
  7. data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
  8. data/app/models/scimitar/engine_configuration.rb +3 -1
  9. data/app/models/scimitar/lists/query_parser.rb +88 -3
  10. data/app/models/scimitar/resources/base.rb +36 -5
  11. data/app/models/scimitar/resources/mixin.rb +133 -43
  12. data/app/models/scimitar/schema/name.rb +2 -2
  13. data/app/models/scimitar/schema/user.rb +10 -10
  14. data/config/initializers/scimitar.rb +41 -0
  15. data/lib/scimitar/engine.rb +57 -12
  16. data/lib/scimitar/support/utilities.rb +60 -0
  17. data/lib/scimitar/version.rb +2 -2
  18. data/spec/apps/dummy/app/models/mock_user.rb +18 -3
  19. data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
  20. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
  21. data/spec/apps/dummy/db/schema.rb +1 -0
  22. data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
  23. data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
  24. data/spec/models/scimitar/resources/base_spec.rb +11 -11
  25. data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
  26. data/spec/models/scimitar/resources/mixin_spec.rb +71 -10
  27. data/spec/models/scimitar/schema/user_spec.rb +2 -2
  28. data/spec/requests/active_record_backed_resources_controller_spec.rb +231 -0
  29. data/spec/requests/engine_spec.rb +75 -0
  30. data/spec/spec_helper.rb +1 -1
  31. metadata +22 -22
@@ -1,15 +1,38 @@
1
+ require 'rails/engine'
2
+
1
3
  module Scimitar
2
4
  class Engine < ::Rails::Engine
3
5
  isolate_namespace Scimitar
4
6
 
7
+ config.autoload_once_paths = %W(
8
+ #{root}/app/controllers
9
+ #{root}/app/models
10
+ )
11
+
5
12
  Mime::Type.register 'application/scim+json', :scim
6
13
 
7
14
  ActionDispatch::Request.parameter_parsers[Mime::Type.lookup('application/scim+json').symbol] = lambda do |body|
8
15
  JSON.parse(body)
9
16
  end
10
17
 
18
+ # Return an Array of all supported default and custom resource classes.
19
+ # See also :add_custom_resource and :set_default_resources.
20
+ #
11
21
  def self.resources
12
- default_resources + custom_resources
22
+ self.default_resources() + self.custom_resources()
23
+ end
24
+
25
+ # Returns a flat array of instances of all resource schema included in the
26
+ # resource classes returned by ::resources.
27
+ #
28
+ def self.schemas
29
+ self.resources().map(&:schemas).flatten.uniq.map(&:new)
30
+ end
31
+
32
+ # Returns the list of custom resources, if any.
33
+ #
34
+ def self.custom_resources
35
+ @custom_resources ||= []
13
36
  end
14
37
 
15
38
  # Can be used to add a new resource type which is not provided by the gem.
@@ -30,7 +53,7 @@ module Scimitar
30
53
  # Scimitar::Engine.add_custom_resource Scim::Resources::ShinyResource
31
54
  #
32
55
  def self.add_custom_resource(resource)
33
- custom_resources << resource
56
+ self.custom_resources() << resource
34
57
  end
35
58
 
36
59
  # Resets the resource list to default. This is really only intended for use
@@ -40,23 +63,45 @@ module Scimitar
40
63
  @custom_resources = []
41
64
  end
42
65
 
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:
66
+ # Returns the default resources added in this gem - by default, these are:
50
67
  #
51
68
  # * Scimitar::Resources::User
52
69
  # * Scimitar::Resources::Group
53
70
  #
71
+ # ...but if an implementation does not e.g. support Group, it can
72
+ # be overridden via ::set_default_resources to help with service
73
+ # auto-discovery.
74
+ #
54
75
  def self.default_resources
55
- [ Resources::User, Resources::Group ]
76
+ @standard_default_resources = [ Resources::User, Resources::Group ]
77
+ @default_resources ||= @standard_default_resources.dup()
56
78
  end
57
79
 
58
- def self.schemas
59
- resources.map(&:schemas).flatten.uniq.map(&:new)
80
+ # Override the resources returned by ::default_resources.
81
+ #
82
+ # +resource_array+:: An Array containing one or both of
83
+ # Scimitar::Resources::User and/or
84
+ # Scimitar::Resources::Group, and nothing else.
85
+ #
86
+ def self.set_default_resources(resource_array)
87
+ self.default_resources()
88
+ unrecognised_resources = resource_array - @standard_default_resources
89
+
90
+ if unrecognised_resources.any?
91
+ raise "Scimitar::Engine::set_default_resources: Only #{@standard_default_resources.map(&:name).join(', ')} are supported"
92
+ elsif resource_array.empty?
93
+ raise 'Scimitar::Engine::set_default_resources: At least one resource must be given'
94
+ end
95
+
96
+ @default_resources = resource_array
97
+ end
98
+
99
+ # Resets the default resource list. This is really only intended for use
100
+ # during testing, to avoid one test polluting another.
101
+ #
102
+ def self.reset_default_resources
103
+ self.default_resources()
104
+ @default_resources = @standard_default_resources
60
105
  end
61
106
 
62
107
  end
@@ -46,6 +46,66 @@ module Scimitar
46
46
  hash[array.shift()] = self.dot_path(array, value)
47
47
  end
48
48
  end
49
+
50
+ # Schema ID-aware splitter handling ":" or "." separators. Adapted from
51
+ # contribution by @bettysteger and @MorrisFreeman in:
52
+ #
53
+ # https://github.com/RIPAGlobal/scimitar/issues/48
54
+ # https://github.com/RIPAGlobal/scimitar/pull/49
55
+ #
56
+ # +schemas:: Array of extension schemas, e.g. a SCIM resource class'
57
+ # <tt>scim_resource_type.extended_schemas</tt> value. The
58
+ # Array should be empty if there are no extensions.
59
+ #
60
+ # +path_str+:: Path String, e.g. <tt>"password"</tt>, <tt>"name.givenName"</tt>,
61
+ # <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"</tt> (special case),
62
+ # <tt>"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization"</tt>
63
+ # (if given a Symbol, it'll be converted to a String).
64
+ #
65
+ # Returns an array of components, e.g. <tt>["password"]</tt>, <tt>["name",
66
+ # "givenName"]</tt>,
67
+ # <tt>["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]</tt> (special case),
68
+ # <tt>["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", "organization"]</tt>.
69
+ #
70
+ # The called-out special case is for a schema ID without any appended
71
+ # path components, which is returned as a single element ID to aid in
72
+ # traversal particularly of things like PATCH requests. There, a "value"
73
+ # attribute might have a key string that's simply a schema ID, with an
74
+ # object beneath that's got attribute-name pairs, possibly nested, in a
75
+ # path-free payload.
76
+ #
77
+ def self.path_str_to_array(schemas, path_str)
78
+ path_str = path_str.to_s
79
+ components = []
80
+
81
+ # Note the ":" separating the schema ID (URN) from the attribute.
82
+ # The nature of JSON rendering / other payloads might lead you to
83
+ # expect a "." as with any complex types, but that's not the case;
84
+ # see https://tools.ietf.org/html/rfc7644#section-3.10, or
85
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2 of which in
86
+ # particular, https://tools.ietf.org/html/rfc7644#page-35.
87
+ #
88
+ if path_str.include?(':')
89
+ lower_case_path_str = path_str.downcase()
90
+
91
+ schemas.each do |schema|
92
+ lower_case_schema_id = schema.id.downcase()
93
+ attributes_after_schema_id = lower_case_path_str.split(lower_case_schema_id + ':').drop(1)
94
+
95
+ if attributes_after_schema_id.empty?
96
+ components += [schema.id] if lower_case_path_str == lower_case_schema_id
97
+ else
98
+ attributes_after_schema_id.each do |component|
99
+ components += [schema.id] + component.split('.')
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ components = path_str.split('.') if components.empty?
106
+ return components
107
+ end
108
+
49
109
  end
50
110
  end
51
111
  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.8.2'
6
+ VERSION = '1.10.0'
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 = '2024-03-27'
11
+ DATE = '2024-06-27'
12
12
 
13
13
  end
@@ -18,6 +18,7 @@ class MockUser < ActiveRecord::Base
18
18
  work_phone_number
19
19
  organization
20
20
  department
21
+ manager
21
22
  mock_groups
22
23
  }
23
24
 
@@ -48,6 +49,7 @@ class MockUser < ActiveRecord::Base
48
49
  externalId: :scim_uid,
49
50
  userName: :username,
50
51
  password: :password,
52
+ active: :is_active,
51
53
  name: {
52
54
  givenName: :first_name,
53
55
  familyName: :last_name
@@ -80,8 +82,11 @@ class MockUser < ActiveRecord::Base
80
82
  }
81
83
  },
82
84
  ],
83
- groups: [ # NB read-only, so no :find_with key
85
+ groups: [
84
86
  {
87
+ # Read-only, so no :find_with key. There's no 'class' specified here
88
+ # either, to help test the "/Schemas" endpoint's reflection code.
89
+ #
85
90
  list: :mock_groups,
86
91
  using: {
87
92
  value: :id,
@@ -89,13 +94,16 @@ class MockUser < ActiveRecord::Base
89
94
  }
90
95
  }
91
96
  ],
92
- active: :is_active,
93
97
 
94
98
  # Custom extension schema - see configuration in
95
99
  # "spec/apps/dummy/config/initializers/scimitar.rb".
96
100
  #
97
101
  organization: :organization,
98
102
  department: :department,
103
+ primaryEmail: :scim_primary_email,
104
+
105
+ manager: :manager,
106
+
99
107
  userGroups: [
100
108
  {
101
109
  list: :mock_groups,
@@ -124,9 +132,16 @@ class MockUser < ActiveRecord::Base
124
132
  'groups.value' => { column: MockGroup.arel_table[:id] },
125
133
  'emails' => { columns: [ :work_email_address, :home_email_address ] },
126
134
  'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
127
- 'emails.type' => { ignore: true } # We can't filter on that; it'll just search all e-mails
135
+ 'emails.type' => { ignore: true }, # We can't filter on that; it'll just search all e-mails
136
+ 'primaryEmail' => { column: :scim_primary_email },
128
137
  }
129
138
  end
130
139
 
140
+ # Custom attribute reader
141
+ #
142
+ def scim_primary_email
143
+ work_email_address
144
+ end
145
+
131
146
  include Scimitar::Resources::Mixin
132
147
  end
@@ -40,10 +40,13 @@ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
40
40
 
41
41
  module ScimSchemaExtensions
42
42
  module User
43
+
44
+ # This "looks like" part of the standard Enterprise extension.
45
+ #
43
46
  class Enterprise < Scimitar::Schema::Base
44
47
  def initialize(options = {})
45
48
  super(
46
- name: 'ExtendedUser',
49
+ name: 'EnterpriseExtendedUser',
47
50
  description: 'Enterprise extension for a User',
48
51
  id: self.class.id,
49
52
  scim_attributes: self.class.scim_attributes
@@ -57,7 +60,32 @@ module ScimSchemaExtensions
57
60
  def self.scim_attributes
58
61
  [
59
62
  Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
60
- Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
63
+ Scimitar::Schema::Attribute.new(name: 'department', type: 'string'),
64
+ Scimitar::Schema::Attribute.new(name: 'primaryEmail', type: 'string'),
65
+ ]
66
+ end
67
+ end
68
+
69
+ # In https://github.com/RIPAGlobal/scimitar/issues/122 we learn that with
70
+ # more than one extension, things can go wrong - so now we test with two.
71
+ #
72
+ class Manager < Scimitar::Schema::Base
73
+ def initialize(options = {})
74
+ super(
75
+ name: 'ManagementExtendedUser',
76
+ description: 'Management extension for a User',
77
+ id: self.class.id,
78
+ scim_attributes: self.class.scim_attributes
79
+ )
80
+ end
81
+
82
+ def self.id
83
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User'
84
+ end
85
+
86
+ def self.scim_attributes
87
+ [
88
+ Scimitar::Schema::Attribute.new(name: 'manager', type: 'string')
61
89
  ]
62
90
  end
63
91
  end
@@ -65,3 +93,4 @@ module ScimSchemaExtensions
65
93
  end
66
94
 
67
95
  Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Enterprise
96
+ Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Manager
@@ -19,6 +19,7 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
19
19
  #
20
20
  t.text :organization
21
21
  t.text :department
22
+ t.text :manager
22
23
  end
23
24
  end
24
25
  end
@@ -42,6 +42,7 @@ ActiveRecord::Schema.define(version: 2021_03_08_044214) do
42
42
  t.text "work_phone_number"
43
43
  t.text "organization"
44
44
  t.text "department"
45
+ t.text "manager"
45
46
  end
46
47
 
47
48
  add_foreign_key "mock_groups_users", "mock_groups"