scimitar 1.8.2 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
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"