scimitar 2.5.0 → 2.11.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +721 -0
  4. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +72 -18
  5. data/app/controllers/scimitar/application_controller.rb +17 -9
  6. data/app/controllers/scimitar/resource_types_controller.rb +7 -3
  7. data/app/controllers/scimitar/resources_controller.rb +0 -2
  8. data/app/controllers/scimitar/schemas_controller.rb +366 -3
  9. data/app/controllers/scimitar/service_provider_configurations_controller.rb +3 -2
  10. data/app/models/scimitar/complex_types/address.rb +0 -6
  11. data/app/models/scimitar/complex_types/base.rb +2 -2
  12. data/app/models/scimitar/engine_configuration.rb +3 -1
  13. data/app/models/scimitar/lists/query_parser.rb +97 -12
  14. data/app/models/scimitar/resource_invalid_error.rb +1 -1
  15. data/app/models/scimitar/resource_type.rb +4 -6
  16. data/app/models/scimitar/resources/base.rb +52 -8
  17. data/app/models/scimitar/resources/mixin.rb +539 -76
  18. data/app/models/scimitar/schema/attribute.rb +18 -8
  19. data/app/models/scimitar/schema/base.rb +2 -2
  20. data/app/models/scimitar/schema/name.rb +2 -2
  21. data/app/models/scimitar/schema/user.rb +10 -10
  22. data/config/initializers/scimitar.rb +49 -3
  23. data/lib/scimitar/engine.rb +57 -12
  24. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
  25. data/lib/scimitar/support/utilities.rb +111 -0
  26. data/lib/scimitar/version.rb +2 -2
  27. data/lib/scimitar.rb +1 -0
  28. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
  29. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
  30. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
  31. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
  32. data/spec/apps/dummy/app/models/mock_user.rb +20 -3
  33. data/spec/apps/dummy/config/application.rb +8 -0
  34. data/spec/apps/dummy/config/initializers/scimitar.rb +40 -3
  35. data/spec/apps/dummy/config/routes.rb +18 -1
  36. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +2 -0
  37. data/spec/apps/dummy/db/schema.rb +3 -1
  38. data/spec/controllers/scimitar/application_controller_spec.rb +56 -2
  39. data/spec/controllers/scimitar/resource_types_controller_spec.rb +8 -4
  40. data/spec/controllers/scimitar/schemas_controller_spec.rb +344 -48
  41. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +1 -0
  42. data/spec/models/scimitar/complex_types/address_spec.rb +3 -4
  43. data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
  44. data/spec/models/scimitar/resources/base_spec.rb +55 -13
  45. data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
  46. data/spec/models/scimitar/resources/mixin_spec.rb +781 -124
  47. data/spec/models/scimitar/schema/attribute_spec.rb +22 -0
  48. data/spec/models/scimitar/schema/user_spec.rb +2 -2
  49. data/spec/requests/active_record_backed_resources_controller_spec.rb +723 -40
  50. data/spec/requests/engine_spec.rb +75 -0
  51. data/spec/spec_helper.rb +10 -2
  52. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
  53. metadata +42 -34
@@ -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
@@ -10,6 +10,7 @@ class MockUser < ActiveRecord::Base
10
10
  primary_key
11
11
  scim_uid
12
12
  username
13
+ password
13
14
  first_name
14
15
  last_name
15
16
  work_email_address
@@ -17,6 +18,7 @@ class MockUser < ActiveRecord::Base
17
18
  work_phone_number
18
19
  organization
19
20
  department
21
+ manager
20
22
  mock_groups
21
23
  }
22
24
 
@@ -46,6 +48,8 @@ class MockUser < ActiveRecord::Base
46
48
  id: :primary_key,
47
49
  externalId: :scim_uid,
48
50
  userName: :username,
51
+ password: :password,
52
+ active: :is_active,
49
53
  name: {
50
54
  givenName: :first_name,
51
55
  familyName: :last_name
@@ -78,8 +82,11 @@ class MockUser < ActiveRecord::Base
78
82
  }
79
83
  },
80
84
  ],
81
- groups: [ # NB read-only, so no :find_with key
85
+ groups: [
82
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
+ #
83
90
  list: :mock_groups,
84
91
  using: {
85
92
  value: :id,
@@ -87,13 +94,16 @@ class MockUser < ActiveRecord::Base
87
94
  }
88
95
  }
89
96
  ],
90
- active: :is_active,
91
97
 
92
98
  # Custom extension schema - see configuration in
93
99
  # "spec/apps/dummy/config/initializers/scimitar.rb".
94
100
  #
95
101
  organization: :organization,
96
102
  department: :department,
103
+ primaryEmail: :scim_primary_email,
104
+
105
+ manager: :manager,
106
+
97
107
  userGroups: [
98
108
  {
99
109
  list: :mock_groups,
@@ -122,9 +132,16 @@ class MockUser < ActiveRecord::Base
122
132
  'groups.value' => { column: MockGroup.arel_table[:id] },
123
133
  'emails' => { columns: [ :work_email_address, :home_email_address ] },
124
134
  'emails.value' => { columns: [ :work_email_address, :home_email_address ] },
125
- '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 },
126
137
  }
127
138
  end
128
139
 
140
+ # Custom attribute reader
141
+ #
142
+ def scim_primary_email
143
+ work_email_address
144
+ end
145
+
129
146
  include Scimitar::Resources::Mixin
130
147
  end
@@ -13,6 +13,14 @@ require 'scimitar'
13
13
  module Dummy
14
14
  class Application < Rails::Application
15
15
  config.load_defaults 7.0
16
+
17
+ # Silence the following under Rails 8.0:
18
+ #
19
+ # "DEPRECATION WARNING: `to_time` will always preserve the full timezone
20
+ # rather than offset of the receiver in Rails 8.1. To opt in to the new
21
+ # behavior, set `config.active_support.to_time_preserves_timezone = :zone`"
22
+ #
23
+ config.active_support.to_time_preserves_timezone = :zone
16
24
  end
17
25
  end
18
26
 
@@ -1,6 +1,6 @@
1
1
  # Test app configuration.
2
2
  #
3
- # Note that as a result of https://github.com/RIPAGlobal/scimitar/issues/48,
3
+ # Note that as a result of https://github.com/pond/scimitar/issues/48,
4
4
  # tests include a custom extension of the core User schema. A shortcoming of
5
5
  # some of the code from which Scimitar was originally built is that those
6
6
  # extensions are done with class-level ivars, so it is largely impossible (or
@@ -19,16 +19,27 @@ Rails.application.config.to_prepare do
19
19
  before_action :test_hook
20
20
  end
21
21
  end
22
+
23
+ def scim_schemas_url(options)
24
+ super(test: 1, **options)
25
+ end
26
+
27
+ def scim_resource_type_url(options)
28
+ super(test: 1, **options)
29
+ end
22
30
  end
23
31
 
24
32
  })
25
33
 
26
34
  module ScimSchemaExtensions
27
35
  module User
36
+
37
+ # This "looks like" part of the standard Enterprise extension.
38
+ #
28
39
  class Enterprise < Scimitar::Schema::Base
29
40
  def initialize(options = {})
30
41
  super(
31
- name: 'ExtendedUser',
42
+ name: 'EnterpriseExtendedUser',
32
43
  description: 'Enterprise extension for a User',
33
44
  id: self.class.id,
34
45
  scim_attributes: self.class.scim_attributes
@@ -42,7 +53,32 @@ Rails.application.config.to_prepare do
42
53
  def self.scim_attributes
43
54
  [
44
55
  Scimitar::Schema::Attribute.new(name: 'organization', type: 'string'),
45
- Scimitar::Schema::Attribute.new(name: 'department', type: 'string')
56
+ Scimitar::Schema::Attribute.new(name: 'department', type: 'string'),
57
+ Scimitar::Schema::Attribute.new(name: 'primaryEmail', type: 'string'),
58
+ ]
59
+ end
60
+ end
61
+
62
+ # In https://github.com/pond/scimitar/issues/122 we learn that with
63
+ # more than one extension, things can go wrong - so now we test with two.
64
+ #
65
+ class Manager < Scimitar::Schema::Base
66
+ def initialize(options = {})
67
+ super(
68
+ name: 'ManagementExtendedUser',
69
+ description: 'Management extension for a User',
70
+ id: self.class.id,
71
+ scim_attributes: self.class.scim_attributes
72
+ )
73
+ end
74
+
75
+ def self.id
76
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User'
77
+ end
78
+
79
+ def self.scim_attributes
80
+ [
81
+ Scimitar::Schema::Attribute.new(name: 'manager', type: 'string')
46
82
  ]
47
83
  end
48
84
  end
@@ -50,4 +86,5 @@ Rails.application.config.to_prepare do
50
86
  end
51
87
 
52
88
  Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Enterprise
89
+ Scimitar::Resources::User.extend_schema ScimSchemaExtensions::User::Manager
53
90
  end
@@ -17,10 +17,27 @@ Rails.application.routes.draw do
17
17
  get 'Groups/:id', to: 'mock_groups#show'
18
18
  patch 'Groups/:id', to: 'mock_groups#update'
19
19
 
20
- # For testing blocks passed to ActiveRecordBackedResourcesController#destroy
20
+ # For testing blocks passed to ActiveRecordBackedResourcesController#create,
21
+ # #update, #replace and #destroy.
21
22
  #
23
+ post 'CustomCreateUsers', to: 'custom_create_mock_users#create'
24
+ patch 'CustomUpdateUsers/:id', to: 'custom_update_mock_users#update'
25
+ put 'CustomReplaceUsers/:id', to: 'custom_replace_mock_users#replace'
22
26
  delete 'CustomDestroyUsers/:id', to: 'custom_destroy_mock_users#destroy'
23
27
 
28
+ # Needed because the auto-render of most of the above includes a 'url_for'
29
+ # call for a 'show' action, so we must include routes (implemented in the
30
+ # base class) for the "show" endpoint.
31
+ #
32
+ get 'CustomCreateUsers/:id', to: 'custom_create_mock_users#show'
33
+ get 'CustomUpdateUsers/:id', to: 'custom_update_mock_users#show'
34
+ get 'CustomReplaceUsers/:id', to: 'custom_replace_mock_users#show'
35
+
36
+ # For testing blocks passed to ActiveRecordBackedResourcesController#save!
37
+ #
38
+ post 'CustomSaveUsers', to: 'custom_save_mock_users#create'
39
+ get 'CustomSaveUsers/:id', to: 'custom_save_mock_users#show'
40
+
24
41
  # For testing environment inside Scimitar::ApplicationController subclasses.
25
42
  #
26
43
  get 'CustomRequestVerifiers', to: 'custom_request_verifiers#index'
@@ -7,6 +7,7 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
7
7
  #
8
8
  t.text :scim_uid
9
9
  t.text :username
10
+ t.text :password
10
11
  t.text :first_name
11
12
  t.text :last_name
12
13
  t.text :work_email_address
@@ -18,6 +19,7 @@ class CreateMockUsers < ActiveRecord::Migration[6.1]
18
19
  #
19
20
  t.text :organization
20
21
  t.text :department
22
+ t.text :manager
21
23
  end
22
24
  end
23
25
  end
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema[7.0].define(version: 2021_03_08_044214) do
13
+ ActiveRecord::Schema[7.1].define(version: 2021_03_08_044214) do
14
14
  # These are extensions that must be enabled in order to support this database
15
15
  enable_extension "plpgsql"
16
16
 
@@ -33,6 +33,7 @@ ActiveRecord::Schema[7.0].define(version: 2021_03_08_044214) do
33
33
  t.datetime "updated_at", null: false
34
34
  t.text "scim_uid"
35
35
  t.text "username"
36
+ t.text "password"
36
37
  t.text "first_name"
37
38
  t.text "last_name"
38
39
  t.text "work_email_address"
@@ -40,6 +41,7 @@ ActiveRecord::Schema[7.0].define(version: 2021_03_08_044214) do
40
41
  t.text "work_phone_number"
41
42
  t.text "organization"
42
43
  t.text "department"
44
+ t.text "manager"
43
45
  end
44
46
 
45
47
  add_foreign_key "mock_groups_users", "mock_groups"
@@ -24,7 +24,7 @@ RSpec.describe Scimitar::ApplicationController do
24
24
  get :index, params: { format: :scim }
25
25
  expect(response).to be_ok
26
26
  expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
27
- expect(response.headers['WWW_AUTHENTICATE']).to eql('Basic')
27
+ expect(response.headers['WWW-Authenticate']).to eql('Basic')
28
28
  end
29
29
 
30
30
  it 'renders failure with bad password' do
@@ -84,7 +84,61 @@ RSpec.describe Scimitar::ApplicationController do
84
84
  get :index, params: { format: :scim }
85
85
  expect(response).to be_ok
86
86
  expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
87
- expect(response.headers['WWW_AUTHENTICATE']).to eql('Bearer')
87
+ expect(response.headers['WWW-Authenticate']).to eql('Bearer')
88
+ end
89
+
90
+ it 'renders failure with bad token' do
91
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer Invalid'
92
+
93
+ get :index, params: { format: :scim }
94
+ expect(response).not_to be_ok
95
+ end
96
+
97
+ it 'renders failure with blank token' do
98
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer'
99
+
100
+ get :index, params: { format: :scim }
101
+ expect(response).not_to be_ok
102
+ end
103
+
104
+ it 'renders failure with missing header' do
105
+ get :index, params: { format: :scim }
106
+ expect(response).not_to be_ok
107
+ end
108
+ end
109
+
110
+ context 'authenticator evaluated within controller context' do
111
+
112
+ # Define a controller with a custom instance method 'valid_token'.
113
+ #
114
+ controller do
115
+ def index
116
+ render json: { 'message' => 'cool, cool!' }, format: :scim
117
+ end
118
+
119
+ def valid_token
120
+ 'B'
121
+ end
122
+ end
123
+
124
+ # Call the above controller method from the token authenticator Proc,
125
+ # proving that it was executed in the controller's context.
126
+ #
127
+ before do
128
+ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new(
129
+ token_authenticator: Proc.new do | token, options |
130
+ token == self.valid_token()
131
+ end
132
+ )
133
+ end
134
+
135
+ it 'renders success when valid creds are given' do
136
+ request.env['HTTP_AUTHORIZATION'] = 'Bearer B'
137
+
138
+ get :index, params: { format: :scim }
139
+ expect(response).to be_ok
140
+ expect(JSON.parse(response.body)).to eql({ 'message' => 'cool, cool!' })
141
+ expect(response.headers['WWW-Authenticate']).to eql('Bearer')
88
142
  end
89
143
 
90
144
  it 'renders failure with bad token' do
@@ -9,11 +9,15 @@ RSpec.describe Scimitar::ResourceTypesController do
9
9
  it 'renders the resource type for user' do
10
10
  get :index, format: :scim
11
11
  response_hash = JSON.parse(response.body)
12
- expected_response = [ Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User')),
13
- Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group'))
14
- ].to_json
12
+ expected_response = {
13
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
14
+ totalResults: 2,
15
+ Resources: [
16
+ Scimitar::Resources::User.resource_type(scim_resource_type_url(name: 'User', test: 1)),
17
+ Scimitar::Resources::Group.resource_type(scim_resource_type_url(name: 'Group', test: 1))
18
+ ]
19
+ }.to_json
15
20
 
16
- response_hash = JSON.parse(response.body)
17
21
  expect(response_hash).to eql(JSON.parse(expected_response))
18
22
  end
19
23