scimaenaga 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1fa2914fdf977f0496f84a023f14ba49f61c893683fd04a85ca7ea4a35c4641
4
- data.tar.gz: 0c23e5d65e10a641d965e34a0cc78fc72c0c5d9e80a438ba86a1aa59f70e4360
3
+ metadata.gz: 3707544f845a4f536dcb1b6bfee0d49c57b6a3108caf0683e24f8c11e3b21f14
4
+ data.tar.gz: b0fb686add4255d36fa4f906b4963ffc5d1b573317f2561f469d4e7cbd8b1f12
5
5
  SHA512:
6
- metadata.gz: 98e1cecc1b60630c69c78380a59492f9dd293c25b267f16d985bcb76570cac0549648781dd9d3f749713df9e40df329349f88708af1e953ec65b7b18ec560648
7
- data.tar.gz: 86d28c8634b38f8af5d85d7670b27e3e7caac314dc90131cd6de9910a284a66dd7a5763ee222c0a8c5adfcafc382b4a919c082fe7f150614359c9f95d1535e59
6
+ metadata.gz: 55612e839d248cbdf14100babd315839fca0b0b6494046201c9fa5a21435634b66a9672740a2187f1cf0bf8c72f46fb4428669b067e20f41453c9dbcfad7e19c
7
+ data.tar.gz: 6c5ffa9c7c769ceedaf08533789ed0864521deea11b2ebad56aa51e941bcdc3512b7027145f5f22c7aecf2359b7745cde59dc8fefc0a0dc0089ea2fff66d3655
data/MIT-LICENSE CHANGED
@@ -1,4 +1,5 @@
1
1
  Copyright 2018 Lessonly
2
+ Copyright 2021 Studist Corporation
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining
4
5
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Scimaenaga
4
4
 
5
- NOTE: This Gem is not yet fully SCIM complaint. It was developed with the main function of interfacing with Okta. There are features of SCIM that this Gem does not implement as described in the SCIM documentation or that have been left out completely.
5
+ NOTE: This Gem is not yet fully SCIM complaint. It was developed with the main function of interfacing with Azure AD. There are features of SCIM that this Gem does not implement as described in the SCIM documentation or that have been left out completely.
6
6
 
7
7
  #### What is SCIM?
8
8
 
@@ -10,7 +10,7 @@ SCIM stands for System for Cross-domain Identity Management. At its core, it is
10
10
 
11
11
  To learn more about SCIM 2.0 you can read the documentation at [RFC 7643](https://tools.ietf.org/html/rfc7643) and [RFC 7644](https://tools.ietf.org/html/rfc7644).
12
12
 
13
- The goal of the Gem is to offer a relatively painless way of adding SCIM 2.0 to your app. This Gem should be fully compatible with Okta's SCIM implementation. This project is ongoing and will hopefully be fully SCIM compliant in time. Pull requests that assist in meeting that goal are welcome!
13
+ The goal of the Gem is to offer a relatively painless way of adding SCIM 2.0 to your app. This Gem should be fully compatible with Azure AD's SCIM implementation. This project is ongoing and will hopefully be fully SCIM compliant in time. Pull requests that assist in meeting that goal are welcome!
14
14
 
15
15
  ## Installation
16
16
 
@@ -236,18 +236,6 @@ Sample request:
236
236
  $ curl -X PUT 'http://username:password@localhost:3000/scim/v2/Users/1' -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"test@example.com","name":{"givenName":"Test","familyName":"User"},"emails":[{"primary":true,"value":"test@example.com","type":"work"}],"displayName":"Test User","active":true}' -H 'Content-Type: application/scim+json'
237
237
  ```
238
238
 
239
- ### Deprovision / Reprovision
240
-
241
- The PATCH request was implemented to work with Okta. Okta updates profiles with PUT and deprovisions / reprovisions with PATCH. This implementation of PATCH is not SCIM compliant as it does not update a single attribute on the user profile but instead only sends a status update request to the record.
242
-
243
- We would like to implement PATCH to be fully SCIM compliant in future releases.
244
-
245
- Sample request:
246
-
247
- ```bash
248
- $ curl -X PATCH 'http://username:password@localhost:3000/scim/v2/Users/1' -d '{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [{"op": "replace", "value": { "active": false }}]}' -H 'Content-Type: application/scim+json'
249
- ```
250
-
251
239
  ### Error Handling
252
240
 
253
241
  By default, scimaenaga will output any unhandled exceptions to your configured rails logs.
@@ -9,17 +9,19 @@ module ScimRails
9
9
  )
10
10
 
11
11
  groups = @company
12
- .public_send(ScimRails.config.scim_groups_scope)
13
- .where(
14
- "#{ScimRails.config.scim_groups_model.connection.quote_column_name(query.attribute)} #{query.operator} ?",
15
- query.parameter
16
- )
17
- .order(ScimRails.config.scim_groups_list_order)
12
+ .public_send(ScimRails.config.scim_groups_scope)
13
+ .where(
14
+ "#{ScimRails.config.scim_groups_model
15
+ .connection.quote_column_name(query.attribute)}
16
+ #{query.operator} ?",
17
+ query.parameter
18
+ )
19
+ .order(ScimRails.config.scim_groups_list_order)
18
20
  else
19
21
  groups = @company
20
- .public_send(ScimRails.config.scim_groups_scope)
21
- .preload(:users)
22
- .order(ScimRails.config.scim_groups_list_order)
22
+ .public_send(ScimRails.config.scim_groups_scope)
23
+ .preload(:users)
24
+ .order(ScimRails.config.scim_groups_list_order)
23
25
  end
24
26
 
25
27
  counts = ScimCount.new(
@@ -33,64 +35,75 @@ module ScimRails
33
35
 
34
36
  def show
35
37
  group = @company
36
- .public_send(ScimRails.config.scim_groups_scope)
37
- .find(params[:id])
38
+ .public_send(ScimRails.config.scim_groups_scope)
39
+ .find(params[:id])
38
40
  json_scim_response(object: group)
39
41
  end
40
42
 
41
43
  def create
42
44
  group = @company
43
- .public_send(ScimRails.config.scim_groups_scope)
44
- .create!(permitted_group_params)
45
+ .public_send(ScimRails.config.scim_groups_scope)
46
+ .create!(permitted_group_params)
45
47
 
46
48
  json_scim_response(object: group, status: :created)
47
49
  end
48
50
 
49
51
  def put_update
50
52
  group = @company
51
- .public_send(ScimRails.config.scim_groups_scope)
52
- .find(params[:id])
53
+ .public_send(ScimRails.config.scim_groups_scope)
54
+ .find(params[:id])
53
55
  group.update!(permitted_group_params)
54
56
  json_scim_response(object: group)
55
57
  end
56
58
 
59
+ def patch_update
60
+ group = @company
61
+ .public_send(ScimRails.config.scim_groups_scope)
62
+ .find(params[:id])
63
+ patch = ScimPatch.new(params, ScimRails.config.mutable_group_attributes_schema)
64
+ patch.save(group)
65
+
66
+ json_scim_response(object: group)
67
+ end
68
+
57
69
  def destroy
58
70
  unless ScimRails.config.group_destroy_method
59
71
  raise ScimRails::ExceptionHandler::UnsupportedDeleteRequest
60
72
  end
73
+
61
74
  group = @company
62
- .public_send(ScimRails.config.scim_groups_scope)
63
- .find(params[:id])
75
+ .public_send(ScimRails.config.scim_groups_scope)
76
+ .find(params[:id])
64
77
  group.public_send(ScimRails.config.group_destroy_method)
65
78
  head :no_content
66
79
  end
67
80
 
68
81
  private
69
82
 
70
- def permitted_group_params
71
- converted = mutable_attributes.each.with_object({}) do |attribute, hash|
72
- hash[attribute] = find_value_for(attribute)
73
- end
74
- return converted unless params[:members]
83
+ def permitted_group_params
84
+ converted = mutable_attributes.each.with_object({}) do |attribute, hash|
85
+ hash[attribute] = find_value_for(attribute)
86
+ end
87
+ return converted unless params[:members]
75
88
 
76
- converted.merge(member_params)
77
- end
89
+ converted.merge(member_params)
90
+ end
78
91
 
79
- def member_params
80
- {
81
- ScimRails.config.group_member_relation_attribute =>
82
- params[:members].map do |member|
83
- member[ScimRails.config.group_member_relation_schema.keys.first]
84
- end
85
- }
86
- end
92
+ def member_params
93
+ {
94
+ ScimRails.config.group_member_relation_attribute =>
95
+ params[:members].map do |member|
96
+ member[ScimRails.config.group_member_relation_schema.keys.first]
97
+ end,
98
+ }
99
+ end
87
100
 
88
- def mutable_attributes
89
- ScimRails.config.mutable_group_attributes
90
- end
101
+ def mutable_attributes
102
+ ScimRails.config.mutable_group_attributes
103
+ end
91
104
 
92
- def controller_schema
93
- ScimRails.config.mutable_group_attributes_schema
94
- end
105
+ def controller_schema
106
+ ScimRails.config.mutable_group_attributes_schema
107
+ end
95
108
  end
96
109
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ScimRails
4
- class ScimUsersController < ScimRails::ApplicationController # rubocop:disable Metrics/ClassLength
5
- # rubocop:disable Metrics/AbcSize
6
- # rubocop:disable Metrics/MethodLength
4
+ class ScimUsersController < ScimRails::ApplicationController
5
+
6
+
7
7
  def index
8
8
  if params[:filter].present?
9
9
  query = ScimRails::ScimQueryParser.new(
@@ -11,17 +11,17 @@ module ScimRails
11
11
  )
12
12
 
13
13
  users = @company
14
- .public_send(ScimRails.config.scim_users_scope)
15
- .where(
16
- "#{ScimRails.config.scim_users_model
14
+ .public_send(ScimRails.config.scim_users_scope)
15
+ .where(
16
+ "#{ScimRails.config.scim_users_model
17
17
  .connection.quote_column_name(query.attribute)} #{query.operator} ?",
18
- query.parameter
19
- )
20
- .order(ScimRails.config.scim_users_list_order)
18
+ query.parameter
19
+ )
20
+ .order(ScimRails.config.scim_users_list_order)
21
21
  else
22
22
  users = @company
23
- .public_send(ScimRails.config.scim_users_scope)
24
- .order(ScimRails.config.scim_users_list_order)
23
+ .public_send(ScimRails.config.scim_users_scope)
24
+ .order(ScimRails.config.scim_users_list_order)
25
25
  end
26
26
 
27
27
  counts = ScimCount.new(
@@ -36,22 +36,22 @@ module ScimRails
36
36
  def create
37
37
  if ScimRails.config.scim_user_prevent_update_on_create
38
38
  user = @company
39
- .public_send(ScimRails.config.scim_users_scope)
40
- .create!(permitted_user_params)
39
+ .public_send(ScimRails.config.scim_users_scope)
40
+ .create!(permitted_user_params)
41
41
  else
42
42
  username_key = ScimRails.config.queryable_user_attributes[:userName]
43
43
  find_by_username = {}
44
44
  find_by_username[username_key] = permitted_user_params[username_key]
45
45
  user = @company
46
- .public_send(ScimRails.config.scim_users_scope)
47
- .find_or_create_by(find_by_username)
46
+ .public_send(ScimRails.config.scim_users_scope)
47
+ .find_or_create_by(find_by_username)
48
48
  user.update!(permitted_user_params)
49
49
  end
50
50
  update_status(user) unless put_active_param.nil?
51
51
  json_scim_response(object: user, status: :created)
52
52
  end
53
- # rubocop:enable Metrics/AbcSize
54
- # rubocop:enable Metrics/MethodLength
53
+
54
+
55
55
 
56
56
  def show
57
57
  user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
@@ -68,8 +68,7 @@ module ScimRails
68
68
  def patch_update
69
69
  user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
70
70
  patch = ScimPatch.new(params, ScimRails.config.mutable_user_attributes_schema)
71
- patch.apply(user)
72
- user.save
71
+ patch.save(user)
73
72
 
74
73
  # update_status(user)
75
74
  json_scim_response(object: user)
@@ -97,9 +96,9 @@ module ScimRails
97
96
  active = patch_active_param if active.nil?
98
97
 
99
98
  case active
100
- when true, "true", 1
99
+ when true, 'true', 1
101
100
  true
102
- when false, "false", 0
101
+ when false, 'false', 0
103
102
  false
104
103
  else
105
104
  raise ActiveRecord::RecordInvalid
@@ -115,19 +114,19 @@ module ScimRails
115
114
  raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
116
115
  end
117
116
 
118
- operations = params["Operations"] || {}
117
+ operations = params['Operations'] || {}
119
118
 
120
119
  valid_operation = operations.find(handle_invalid) do |operation|
121
120
  valid_patch_operation?(operation)
122
121
  end
123
122
 
124
- valid_operation.dig("value", "active")
123
+ valid_operation.dig('value', 'active')
125
124
  end
126
125
 
127
126
  def valid_patch_operation?(operation)
128
- operation["op"].casecmp("replace") &&
129
- operation["value"] &&
130
- [true, false].include?(operation["value"]["active"])
127
+ operation['op'].casecmp('replace') &&
128
+ operation['value'] &&
129
+ [true, false].include?(operation['value']['active'])
131
130
  end
132
131
  end
133
132
  end
@@ -6,23 +6,29 @@ class ScimPatch
6
6
 
7
7
  def initialize(params, mutable_attributes_schema)
8
8
  # FIXME: raise proper error.
9
- unless params["schemas"] == ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]
9
+ unless params['schemas'] == ['urn:ietf:params:scim:api:messages:2.0:PatchOp']
10
10
  raise StandardError
11
11
  end
12
- if params["Operations"].nil?
12
+ if params['Operations'].nil?
13
13
  raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
14
14
  end
15
15
 
16
- @operations = params["Operations"].map do |operation|
17
- ScimPatchOperation.new(operation["op"], operation["path"], operation["value"],
16
+ @operations = params['Operations'].map do |operation|
17
+ ScimPatchOperation.new(operation['op'], operation['path'], operation['value'],
18
18
  mutable_attributes_schema)
19
19
  end
20
20
  end
21
21
 
22
- def apply(model)
23
- @operations.each do |operation|
24
- operation.apply(model)
22
+ def save(model)
23
+ model.transaction do
24
+ @operations.each do |operation|
25
+ operation.save(model)
26
+ end
25
27
  end
26
- model
28
+ model.save if model.changed?
29
+ rescue ActiveRecord::RecordNotFound
30
+ raise
31
+ rescue StandardError
32
+ raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
27
33
  end
28
34
  end
@@ -19,18 +19,43 @@ class ScimPatchOperation
19
19
  @value = value
20
20
  end
21
21
 
22
- # WIP
23
- def apply(model)
22
+
23
+
24
+
25
+ def save(model)
26
+ if @path_scim == 'members' # Only members are supported for value is an array
27
+ update_member_ids = @value.map do |v|
28
+ v[ScimRails.config.group_member_relation_schema.keys.first]
29
+ end
30
+
31
+ current_member_ids = model
32
+ .public_send(ScimRails.config.group_member_relation_attribute)
33
+ case @op
34
+ when :add
35
+ member_ids = current_member_ids.concat(update_member_ids)
36
+ when :replace
37
+ member_ids = current_member_ids.concat(update_member_ids)
38
+ when :remove
39
+ member_ids = current_member_ids - update_member_ids
40
+ end
41
+
42
+ # Only the member addition process is saved by each ids
43
+ model.public_send("#{ScimRails.config.group_member_relation_attribute}=",
44
+ member_ids.uniq)
45
+ return
46
+ end
47
+
24
48
  case @op
25
- when :add
26
- model.attributes = { @path_sp => @value }
27
- when :replace
49
+ when :add, :replace
28
50
  model.attributes = { @path_sp => @value }
29
51
  when :remove
30
52
  model.attributes = { @path_sp => nil }
31
53
  end
32
54
  end
33
55
 
56
+
57
+
58
+
34
59
  private
35
60
 
36
61
  def convert_path(path, mutable_attributes_schema)
@@ -47,8 +72,8 @@ class ScimPatchOperation
47
72
  #
48
73
  # Library ignores filter conditions (like [type eq "work"])
49
74
  # and always uses the first element of the array
50
- dig_keys = path.gsub(/\[(.+?)\]/, ".0").split(".").map do |step|
51
- step == "0" ? 0 : step.to_sym
75
+ dig_keys = path.gsub(/\[(.+?)\]/, '.0').split('.').map do |step|
76
+ step == '0' ? 0 : step.to_sym
52
77
  end
53
78
  mutable_attributes_schema.dig(*dig_keys)
54
79
  end
@@ -5,7 +5,7 @@ module ScimRails
5
5
  attr_accessor :query_elements, :query_attributes
6
6
 
7
7
  def initialize(query_string, queryable_attributes)
8
- self.query_elements = query_string.split
8
+ self.query_elements = query_string.gsub(/\[(.+?)\]/, ".0").split
9
9
  self.query_attributes = queryable_attributes
10
10
  end
11
11
 
@@ -13,9 +13,11 @@ module ScimRails
13
13
  attribute = query_elements[0]
14
14
  raise ScimRails::ExceptionHandler::InvalidQuery if attribute.blank?
15
15
 
16
- attribute = attribute.to_sym
16
+ dig_keys = attribute.split(".").map do |step|
17
+ step == "0" ? 0 : step.to_sym
18
+ end
17
19
 
18
- mapped_attribute = query_attributes[attribute]
20
+ mapped_attribute = query_attributes.dig(*dig_keys)
19
21
  raise ScimRails::ExceptionHandler::InvalidQuery if mapped_attribute.blank?
20
22
 
21
23
  mapped_attribute
data/config/routes.rb CHANGED
@@ -8,5 +8,6 @@ ScimRails::Engine.routes.draw do
8
8
  post 'scim/v2/Groups', action: :create, controller: 'scim_groups'
9
9
  get 'scim/v2/Groups/:id', action: :show, controller: 'scim_groups'
10
10
  put 'scim/v2/Groups/:id', action: :put_update, controller: 'scim_groups'
11
+ patch 'scim/v2/Groups/:id', action: :patch_update, controller: 'scim_groups'
11
12
  delete 'scim/v2/Groups/:id', action: :destroy, controller: 'scim_groups'
12
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ScimRails
4
- VERSION = "0.5.0"
4
+ VERSION = '0.6.0'
5
5
  end
@@ -1,32 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
3
+ require 'spec_helper'
4
4
 
5
5
  RSpec.describe ScimRails::ScimGroupsController, type: :controller do
6
6
  include AuthHelper
7
7
 
8
8
  routes { ScimRails::Engine.routes }
9
9
 
10
- describe "index" do
10
+ describe 'index' do
11
11
  let(:company) { create(:company) }
12
12
 
13
- context "when unauthorized" do
14
- it "returns scim+json content type" do
13
+ context 'when unauthorized' do
14
+ it 'returns scim+json content type' do
15
15
  get :index, as: :json
16
16
 
17
- expect(response.media_type).to eq "application/scim+json"
17
+ expect(response.media_type).to eq 'application/scim+json'
18
18
  end
19
19
 
20
- it "fails with no credentials" do
20
+ it 'fails with no credentials' do
21
21
  get :index, as: :json
22
22
 
23
23
  expect(response.status).to eq 401
24
24
  end
25
25
 
26
- it "fails with invalid credentials" do
27
- request.env["HTTP_AUTHORIZATION"] =
26
+ it 'fails with invalid credentials' do
27
+ request.env['HTTP_AUTHORIZATION'] =
28
28
  ActionController::HttpAuthentication::Basic
29
- .encode_credentials("unauthorized", "123456")
29
+ .encode_credentials('unauthorized', '123456')
30
30
 
31
31
  get :index, as: :json
32
32
 
@@ -34,58 +34,58 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
34
34
  end
35
35
  end
36
36
 
37
- context "when authorized" do
37
+ context 'when authorized' do
38
38
  before :each do
39
39
  http_login(company)
40
40
  end
41
41
 
42
- it "returns scim+json content type" do
42
+ it 'returns scim+json content type' do
43
43
  get :index, as: :json
44
44
 
45
- expect(response.media_type).to eq "application/scim+json"
45
+ expect(response.media_type).to eq 'application/scim+json'
46
46
  end
47
47
 
48
- it "is successful with valid credentials" do
48
+ it 'is successful with valid credentials' do
49
49
  get :index, as: :json
50
50
 
51
51
  expect(response.status).to eq 200
52
52
  end
53
53
 
54
- it "returns all results" do
54
+ it 'returns all results' do
55
55
  create_list(:group, 5, company: company)
56
56
 
57
57
  get :index, as: :json
58
58
  response_body = JSON.parse(response.body)
59
- expect(response_body.dig("schemas", 0)).to(
60
- eq "urn:ietf:params:scim:api:messages:2.0:ListResponse"
59
+ expect(response_body.dig('schemas', 0)).to(
60
+ eq 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
61
61
  )
62
- expect(response_body["totalResults"]).to eq 5
62
+ expect(response_body['totalResults']).to eq 5
63
63
  end
64
64
 
65
- it "defaults to 100 results" do
65
+ it 'defaults to 100 results' do
66
66
  create_list(:group, 300, company: company)
67
67
 
68
68
  get :index, as: :json
69
69
  response_body = JSON.parse(response.body)
70
- expect(response_body["totalResults"]).to eq 300
71
- expect(response_body["Resources"].count).to eq 100
70
+ expect(response_body['totalResults']).to eq 300
71
+ expect(response_body['Resources'].count).to eq 100
72
72
  end
73
73
 
74
- it "paginates results" do
74
+ it 'paginates results' do
75
75
  create_list(:group, 400, company: company)
76
76
  expect(company.groups.first.id).to eq 1
77
77
 
78
78
  get :index, params: {
79
79
  startIndex: 101,
80
- count: 200
80
+ count: 200,
81
81
  }, as: :json
82
82
  response_body = JSON.parse(response.body)
83
- expect(response_body["totalResults"]).to eq 400
84
- expect(response_body["Resources"].count).to eq 200
85
- expect(response_body.dig("Resources", 0, "id")).to eq 101
83
+ expect(response_body['totalResults']).to eq 400
84
+ expect(response_body['Resources'].count).to eq 200
85
+ expect(response_body.dig('Resources', 0, 'id')).to eq 101
86
86
  end
87
87
 
88
- it "paginates results by configurable scim_groups_list_order" do
88
+ it 'paginates results by configurable scim_groups_list_order' do
89
89
  allow(ScimRails.config).to(
90
90
  receive(:scim_groups_list_order).and_return(created_at: :desc)
91
91
  )
@@ -95,69 +95,69 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
95
95
 
96
96
  get :index, params: {
97
97
  startIndex: 1,
98
- count: 10
98
+ count: 10,
99
99
  }, as: :json
100
100
  response_body = JSON.parse(response.body)
101
- expect(response_body["totalResults"]).to eq 400
102
- expect(response_body["Resources"].count).to eq 10
103
- expect(response_body.dig("Resources", 0, "id")).to eq 400
101
+ expect(response_body['totalResults']).to eq 400
102
+ expect(response_body['Resources'].count).to eq 10
103
+ expect(response_body.dig('Resources', 0, 'id')).to eq 400
104
104
  end
105
105
 
106
- it "filters results by provided displayName filter" do
107
- create(:group, name: "Foo", company: company)
108
- create(:group, name: "Bar", company: company)
106
+ it 'filters results by provided displayName filter' do
107
+ create(:group, name: 'Foo', company: company)
108
+ create(:group, name: 'Bar', company: company)
109
109
 
110
110
  get :index, params: {
111
- filter: "displayName eq Bar"
111
+ filter: 'displayName eq Bar',
112
112
  }, as: :json
113
113
  response_body = JSON.parse(response.body)
114
- expect(response_body["totalResults"]).to eq 1
115
- expect(response_body["Resources"].count).to eq 1
116
- expect(response_body.dig("Resources", 0, "displayName")).to eq "Bar"
114
+ expect(response_body['totalResults']).to eq 1
115
+ expect(response_body['Resources'].count).to eq 1
116
+ expect(response_body.dig('Resources', 0, 'displayName')).to eq 'Bar'
117
117
  end
118
118
 
119
- it "returns no results for unfound filter parameters" do
119
+ it 'returns no results for unfound filter parameters' do
120
120
  get :index, params: {
121
- filter: "displayName eq fake_not_there"
121
+ filter: 'displayName eq fake_not_there',
122
122
  }, as: :json
123
123
  response_body = JSON.parse(response.body)
124
- expect(response_body["totalResults"]).to eq 0
125
- expect(response_body["Resources"].count).to eq 0
124
+ expect(response_body['totalResults']).to eq 0
125
+ expect(response_body['Resources'].count).to eq 0
126
126
  end
127
127
 
128
- it "returns no results for undefined filter queries" do
128
+ it 'returns no results for undefined filter queries' do
129
129
  get :index, params: {
130
- filter: "address eq 101 Nowhere USA"
130
+ filter: 'address eq 101 Nowhere USA',
131
131
  }, as: :json
132
132
  expect(response.status).to eq 400
133
133
  response_body = JSON.parse(response.body)
134
- expect(response_body.dig("schemas", 0)).to(
135
- eq "urn:ietf:params:scim:api:messages:2.0:Error"
134
+ expect(response_body.dig('schemas', 0)).to(
135
+ eq 'urn:ietf:params:scim:api:messages:2.0:Error'
136
136
  )
137
137
  end
138
138
  end
139
139
  end
140
140
 
141
- describe "show" do
141
+ describe 'show' do
142
142
  let(:company) { create(:company) }
143
143
 
144
- context "when unauthorized" do
145
- it "returns scim+json content type" do
144
+ context 'when unauthorized' do
145
+ it 'returns scim+json content type' do
146
146
  get :show, params: { id: 1 }, as: :json
147
147
 
148
- expect(response.media_type).to eq "application/scim+json"
148
+ expect(response.media_type).to eq 'application/scim+json'
149
149
  end
150
150
 
151
- it "fails with no credentials" do
151
+ it 'fails with no credentials' do
152
152
  get :show, params: { id: 1 }, as: :json
153
153
 
154
154
  expect(response.status).to eq 401
155
155
  end
156
156
 
157
- it "fails with invalid credentials" do
158
- request.env["HTTP_AUTHORIZATION"] =
157
+ it 'fails with invalid credentials' do
158
+ request.env['HTTP_AUTHORIZATION'] =
159
159
  ActionController::HttpAuthentication::Basic
160
- .encode_credentials("unauthorized", "123456")
160
+ .encode_credentials('unauthorized', '123456')
161
161
 
162
162
  get :show, params: { id: 1 }, as: :json
163
163
 
@@ -165,31 +165,31 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
165
165
  end
166
166
  end
167
167
 
168
- context "when authorized" do
168
+ context 'when authorized' do
169
169
  before :each do
170
170
  http_login(company)
171
171
  end
172
172
 
173
- it "returns scim+json content type" do
173
+ it 'returns scim+json content type' do
174
174
  get :show, params: { id: 1 }, as: :json
175
175
 
176
- expect(response.media_type).to eq "application/scim+json"
176
+ expect(response.media_type).to eq 'application/scim+json'
177
177
  end
178
178
 
179
- it "is successful with valid credentials" do
179
+ it 'is successful with valid credentials' do
180
180
  create(:group, id: 1, company: company)
181
181
  get :show, params: { id: 1 }, as: :json
182
182
 
183
183
  expect(response.status).to eq 200
184
184
  end
185
185
 
186
- it "returns :not_found for id that cannot be found" do
187
- get :show, params: { id: "fake_id" }, as: :json
186
+ it 'returns :not_found for id that cannot be found' do
187
+ get :show, params: { id: 'fake_id' }, as: :json
188
188
 
189
189
  expect(response.status).to eq 404
190
190
  end
191
191
 
192
- it "returns :not_found for a correct id but unauthorized company" do
192
+ it 'returns :not_found for a correct id but unauthorized company' do
193
193
  new_company = create(:company)
194
194
  create(:group, company: new_company, id: 1)
195
195
 
@@ -200,26 +200,26 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
200
200
  end
201
201
  end
202
202
 
203
- describe "create" do
203
+ describe 'create' do
204
204
  let(:company) { create(:company) }
205
205
 
206
- context "when unauthorized" do
207
- it "returns scim+json content type" do
206
+ context 'when unauthorized' do
207
+ it 'returns scim+json content type' do
208
208
  post :create, as: :json
209
209
 
210
- expect(response.media_type).to eq "application/scim+json"
210
+ expect(response.media_type).to eq 'application/scim+json'
211
211
  end
212
212
 
213
- it "fails with no credentials" do
213
+ it 'fails with no credentials' do
214
214
  post :create, as: :json
215
215
 
216
216
  expect(response.status).to eq 401
217
217
  end
218
218
 
219
- it "fails with invalid credentials" do
220
- request.env["HTTP_AUTHORIZATION"] =
219
+ it 'fails with invalid credentials' do
220
+ request.env['HTTP_AUTHORIZATION'] =
221
221
  ActionController::HttpAuthentication::Basic
222
- .encode_credentials("unauthorized", "123456")
222
+ .encode_credentials('unauthorized', '123456')
223
223
 
224
224
  post :create, as: :json
225
225
 
@@ -227,107 +227,107 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
227
227
  end
228
228
  end
229
229
 
230
- context "when authorized" do
230
+ context 'when authorized' do
231
231
  before :each do
232
232
  http_login(company)
233
233
  end
234
234
 
235
- it "returns scim+json content type" do
235
+ it 'returns scim+json content type' do
236
236
  post :create, params: {
237
- displayName: "Test Group",
238
- members: []
237
+ displayName: 'Test Group',
238
+ members: [],
239
239
  }, as: :json
240
240
 
241
- expect(response.media_type).to eq "application/scim+json"
241
+ expect(response.media_type).to eq 'application/scim+json'
242
242
  end
243
243
 
244
- it "is successful with valid credentials" do
244
+ it 'is successful with valid credentials' do
245
245
  expect(company.groups.count).to eq 0
246
246
 
247
247
  post :create, params: {
248
- displayName: "Test Group",
249
- members: []
248
+ displayName: 'Test Group',
249
+ members: [],
250
250
  }, as: :json
251
251
 
252
252
  expect(response.status).to eq 201
253
253
  expect(company.groups.count).to eq 1
254
254
  group = company.groups.first
255
255
  expect(group.persisted?).to eq true
256
- expect(group.name).to eq "Test Group"
256
+ expect(group.name).to eq 'Test Group'
257
257
  expect(group.users).to eq []
258
258
  end
259
259
 
260
- it "ignores unconfigured params" do
260
+ it 'ignores unconfigured params' do
261
261
  post :create, params: {
262
- displayName: "Test Group",
263
- department: "Best Department",
264
- members: []
262
+ displayName: 'Test Group',
263
+ department: 'Best Department',
264
+ members: [],
265
265
  }, as: :json
266
266
 
267
267
  expect(response.status).to eq 201
268
268
  expect(company.groups.count).to eq 1
269
269
  end
270
270
 
271
- it "returns 422 if required params are missing" do
271
+ it 'returns 422 if required params are missing' do
272
272
  post :create, params: {
273
- members: []
273
+ members: [],
274
274
  }, as: :json
275
275
 
276
276
  expect(response.status).to eq 422
277
277
  expect(company.users.count).to eq 0
278
278
  end
279
279
 
280
- it "returns 409 if group already exists" do
281
- create(:group, name: "Test Group", company: company)
280
+ it 'returns 409 if group already exists' do
281
+ create(:group, name: 'Test Group', company: company)
282
282
 
283
283
  post :create, params: {
284
- displayName: "Test Group",
285
- members: []
284
+ displayName: 'Test Group',
285
+ members: [],
286
286
  }, as: :json
287
287
 
288
288
  expect(response.status).to eq 409
289
289
  expect(company.groups.count).to eq 1
290
290
  end
291
291
 
292
- it "creates group" do
292
+ it 'creates group' do
293
293
  users = create_list(:user, 3, company: company)
294
294
 
295
295
  post :create, params: {
296
- displayName: "Test Group",
296
+ displayName: 'Test Group',
297
297
  members: users.map do |user|
298
298
  { value: user.id.to_s, display: user.email }
299
- end
299
+ end,
300
300
  }, as: :json
301
301
 
302
302
  expect(response.status).to eq 201
303
303
  expect(company.groups.count).to eq 1
304
304
  group = company.groups.first
305
- expect(group.name).to eq "Test Group"
305
+ expect(group.name).to eq 'Test Group'
306
306
  expect(group.users.count).to eq 3
307
307
  end
308
308
  end
309
309
  end
310
310
 
311
- describe "put update" do
311
+ describe 'put update' do
312
312
  let(:company) { create(:company) }
313
313
 
314
- context "when unauthorized" do
315
- it "returns scim+json content type" do
314
+ context 'when unauthorized' do
315
+ it 'returns scim+json content type' do
316
316
  put :put_update, params: { id: 1 }, as: :json
317
317
 
318
- expect(response.media_type).to eq "application/scim+json"
318
+ expect(response.media_type).to eq 'application/scim+json'
319
319
  end
320
320
 
321
- it "fails with no credentials" do
321
+ it 'fails with no credentials' do
322
322
  put :put_update, params: { id: 1 }, as: :json
323
323
 
324
324
  expect(response.status).to eq 401
325
325
  end
326
326
 
327
- it "fails with invalid credentials" do
328
- request.env["HTTP_AUTHORIZATION"] =
327
+ it 'fails with invalid credentials' do
328
+ request.env['HTTP_AUTHORIZATION'] =
329
329
  ActionController::HttpAuthentication::Basic
330
- .encode_credentials("unauthorized", "123456")
330
+ .encode_credentials('unauthorized', '123456')
331
331
 
332
332
  put :put_update, params: { id: 1 }, as: :json
333
333
 
@@ -335,26 +335,26 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
335
335
  end
336
336
  end
337
337
 
338
- context "when authorized" do
338
+ context 'when authorized' do
339
339
  let!(:group) { create(:group, id: 1, company: company) }
340
340
 
341
341
  before :each do
342
342
  http_login(company)
343
343
  end
344
344
 
345
- it "returns scim+json content type" do
345
+ it 'returns scim+json content type' do
346
346
  put :put_update, params: put_params, as: :json
347
347
 
348
- expect(response.media_type).to eq "application/scim+json"
348
+ expect(response.media_type).to eq 'application/scim+json'
349
349
  end
350
350
 
351
- it "is successful with with valid credentials" do
351
+ it 'is successful with with valid credentials' do
352
352
  put :put_update, params: put_params, as: :json
353
353
 
354
354
  expect(response.status).to eq 200
355
355
  end
356
356
 
357
- it "can add and delete Users from a Group at once" do
357
+ it 'can add and delete Users from a Group at once' do
358
358
  user1 = create(:user, company: company, groups: [group])
359
359
  user2 = create(:user, company: company)
360
360
 
@@ -365,13 +365,13 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
365
365
  expect(response.status).to eq 200
366
366
  end
367
367
 
368
- it "returns :not_found for id that cannot be found" do
369
- put :put_update, params: { id: "fake_id" }, as: :json
368
+ it 'returns :not_found for id that cannot be found' do
369
+ put :put_update, params: { id: 'fake_id' }, as: :json
370
370
 
371
371
  expect(response.status).to eq 404
372
372
  end
373
373
 
374
- it "returns :not_found for a correct id but unauthorized company" do
374
+ it 'returns :not_found for a correct id but unauthorized company' do
375
375
  new_company = create(:company)
376
376
  create(:group, company: new_company, id: 1000)
377
377
 
@@ -380,10 +380,10 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
380
380
  expect(response.status).to eq 404
381
381
  end
382
382
 
383
- it "returns 422 with incomplete request" do
383
+ it 'returns 422 with incomplete request' do
384
384
  put :put_update, params: {
385
385
  id: 1,
386
- members: []
386
+ members: [],
387
387
  }, as: :json
388
388
 
389
389
  expect(response.status).to eq 422
@@ -391,26 +391,105 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
391
391
  end
392
392
  end
393
393
 
394
- describe "destroy" do
394
+ describe 'patch update' do
395
395
  let(:company) { create(:company) }
396
396
 
397
- context "when unauthorized" do
398
- it "returns scim+json content type" do
397
+ context 'when authorized' do
398
+ let!(:group) { create(:group, id: 1, company: company) }
399
+ let(:user1) { create(:user, company: company, groups: [group]) }
400
+ let(:user2) { create(:user, company: company) }
401
+
402
+ before :each do
403
+ http_login(company)
404
+ end
405
+
406
+ it 'returns scim+json content type' do
407
+ patch :patch_update, params: patch_params, as: :json
408
+
409
+ expect(response.media_type).to eq 'application/scim+json'
410
+ end
411
+
412
+ it 'can change displayName of group' do
413
+ expect do
414
+ patch :patch_update, params: {
415
+ id: group.id,
416
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
417
+ Operations: [{
418
+ op: 'Replace',
419
+ path: 'displayName',
420
+ value: 'changed'
421
+ }]
422
+ }, as: :json
423
+ end.to change { group.reload.name }.to('changed')
424
+
425
+ expect(response.status).to eq 200
426
+ end
427
+
428
+ it 'can add Users from a Group' do
429
+ expect do
430
+ patch :patch_update, params: patch_params(user_id: user2.id), as: :json
431
+ end.to change { group.reload.users }.from([user1]).to([user1, user2])
432
+
433
+ expect(response.status).to eq 200
434
+ end
435
+
436
+ it 'can delete Users from a Group' do
437
+ user1 = create(:user, company: company, groups: [group])
438
+ user2 = create(:user, company: company, groups: [group])
439
+
440
+ expect do
441
+ put :patch_update, params: patch_params(user_id: user2.id, op: 'Remove'),
442
+ as: :json
443
+ end.to change { group.reload.users }.from([user1, user2]).to([user1])
444
+
445
+ expect(response.status).to eq 200
446
+ end
447
+
448
+ it 'returns :not_found for id that cannot be found' do
449
+ patch :patch_update, params: patch_params(user_id: 0), as: :json
450
+
451
+ expect(response.status).to eq 404
452
+ end
453
+
454
+ it 'rollback if even one cannot be saved' do
455
+ expect do
456
+ patch :patch_update, params: {
457
+ id: group.id,
458
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
459
+ Operations: [{
460
+ op: 'Add',
461
+ path: 'members',
462
+ value: [
463
+ { value: user2.id },
464
+ { value: 0 }
465
+ ],
466
+ }],
467
+ }, as: :json
468
+ end.to_not change { group.reload.users.count }
469
+ end
470
+ end
471
+ end
472
+
473
+ describe 'destroy' do
474
+ let(:company) { create(:company) }
475
+
476
+ context 'when unauthorized' do
477
+ it 'returns scim+json content type' do
399
478
  delete :destroy, params: { id: 1 }, as: :json
400
479
 
401
- expect(response.media_type).to eq "application/scim+json"
480
+ expect(response.media_type).to eq 'application/scim+json'
402
481
  end
403
482
 
404
- it "fails with no credentials" do
483
+ it 'fails with no credentials' do
405
484
  delete :destroy, params: { id: 1 }, as: :json
406
485
 
407
486
  expect(response.status).to eq 401
408
487
  end
409
488
 
410
- it "fails with invalid credentials" do
411
- request.env["HTTP_AUTHORIZATION"] =
489
+ it 'fails with invalid credentials' do
490
+ request.env['HTTP_AUTHORIZATION'] =
412
491
  ActionController::HttpAuthentication::Basic
413
- .encode_credentials("unauthorized", "123456")
492
+ .encode_credentials('unauthorized', '123456')
414
493
 
415
494
  delete :destroy, params: { id: 1 }, as: :json
416
495
 
@@ -418,39 +497,39 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
418
497
  end
419
498
  end
420
499
 
421
- context "when authorized" do
500
+ context 'when authorized' do
422
501
  let!(:group) { create(:group, id: 1, company: company) }
423
502
 
424
503
  before :each do
425
504
  http_login(company)
426
505
  end
427
506
 
428
- context "when Group destroy method is configured" do
507
+ context 'when Group destroy method is configured' do
429
508
  before do
430
509
  allow(ScimRails.config).to(
431
510
  receive(:group_destroy_method).and_return(:destroy!)
432
511
  )
433
512
  end
434
513
 
435
- it "returns empty response" do
514
+ it 'returns empty response' do
436
515
  delete :destroy, params: { id: 1 }, as: :json
437
516
 
438
517
  expect(response.body).to be_empty
439
518
  end
440
519
 
441
- it "is successful with valid credentials" do
520
+ it 'is successful with valid credentials' do
442
521
  delete :destroy, params: { id: 1 }, as: :json
443
522
 
444
523
  expect(response.status).to eq 204
445
524
  end
446
525
 
447
- it "returns :not_found for id that cannot be found" do
448
- delete :destroy, params: { id: "fake_id" }, as: :json
526
+ it 'returns :not_found for id that cannot be found' do
527
+ delete :destroy, params: { id: 'fake_id' }, as: :json
449
528
 
450
529
  expect(response.status).to eq 404
451
530
  end
452
531
 
453
- it "returns :not_found for a correct id but unauthorized company" do
532
+ it 'returns :not_found for a correct id but unauthorized company' do
454
533
  new_company = create(:company)
455
534
  create(:group, company: new_company, id: 1000)
456
535
 
@@ -459,7 +538,7 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
459
538
  expect(response.status).to eq 404
460
539
  end
461
540
 
462
- it "successfully deletes Group" do
541
+ it 'successfully deletes Group' do
463
542
  expect do
464
543
  delete :destroy, params: { id: 1 }, as: :json
465
544
  end.to change { company.groups.reload.count }.from(1).to(0)
@@ -468,8 +547,8 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
468
547
  end
469
548
  end
470
549
 
471
- context "when Group destroy method is not configured" do
472
- it "does not delete Group" do
550
+ context 'when Group destroy method is not configured' do
551
+ it 'does not delete Group' do
473
552
  allow(ScimRails.config).to(
474
553
  receive(:group_destroy_method).and_return(nil)
475
554
  )
@@ -484,11 +563,27 @@ RSpec.describe ScimRails::ScimGroupsController, type: :controller do
484
563
  end
485
564
  end
486
565
 
487
- def put_params(name: "Test Group", users: [])
566
+ def put_params(name: 'Test Group', users: [])
488
567
  {
489
568
  id: 1,
490
569
  displayName: name,
491
- members: users.map { |user| { value: user.id.to_s, display: user.email } }
570
+ members: users.map { |user| { value: user.id.to_s, display: user.email } },
571
+ }
572
+ end
573
+
574
+ # rubocop:disable Metrics/MethodLength
575
+ def patch_params(user_id: 1, op: 'Add')
576
+ {
577
+ id: 1,
578
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
579
+ Operations: [{
580
+ op: op,
581
+ path: 'members',
582
+ value: [{
583
+ value: user_id,
584
+ }],
585
+ }],
492
586
  }
493
587
  end
588
+ # rubocop:enable Metrics/MethodLength
494
589
  end
data/spec/dummy/bin/setup CHANGED
@@ -22,6 +22,8 @@ chdir APP_ROOT do
22
22
  # unless File.exist?('config/database.yml')
23
23
  # cp 'config/database.yml.sample', 'config/database.yml'
24
24
  # end
25
+ puts "\n== Reset migration =="
26
+ system! 'bin/rails db:migrate:reset'
25
27
 
26
28
  puts "\n== Preparing database =="
27
29
  system! 'bin/rails db:setup'
@@ -4,6 +4,11 @@ company = Company.create(
4
4
  api_token: 1
5
5
  )
6
6
 
7
+ group = Group.create(
8
+ company: company,
9
+ name: 'Test Group'
10
+ )
11
+
7
12
  1.upto(1000) do |n|
8
13
  User.create(
9
14
  company: company,
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ScimRails::ScimQueryParser do
6
+
7
+ let(:query_string) { 'userName eq "taro"' }
8
+ let(:queryable_attributes) {
9
+ {
10
+ userName: :name,
11
+ emails: [
12
+ {
13
+ value: :email
14
+ }
15
+ ]
16
+ }
17
+ }
18
+ let(:parser) { described_class.new(query_string, queryable_attributes) }
19
+
20
+ describe '#attribute' do
21
+ context 'userName' do
22
+ it { expect(parser.attribute).to eq :name }
23
+ end
24
+
25
+ context 'emails[type eq "work"].value' do
26
+ let(:query_string) { 'emails[type eq "work"].value eq "taro@example.com"' }
27
+ it { expect(parser.attribute).to eq :email }
28
+ end
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scimaenaga
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Studist Corporation
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-06 00:00:00.000000000 Z
11
+ date: 2021-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -237,6 +237,7 @@ files:
237
237
  - spec/lib/scim_rails/encoder_spec.rb
238
238
  - spec/libraries/scim_patch_operation_spec.rb
239
239
  - spec/libraries/scim_patch_spec.rb
240
+ - spec/models/scim_query_parser_spec.rb
240
241
  - spec/spec_helper.rb
241
242
  - spec/support/auth_helper.rb
242
243
  - spec/support/factory_bot.rb
@@ -343,3 +344,4 @@ test_files:
343
344
  - spec/factories/group.rb
344
345
  - spec/factories/company.rb
345
346
  - spec/spec_helper.rb
347
+ - spec/models/scim_query_parser_spec.rb