scimaenaga 0.4.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +314 -0
- data/Rakefile +34 -0
- data/app/controllers/concerns/scim_rails/exception_handler.rb +119 -0
- data/app/controllers/concerns/scim_rails/response.rb +94 -0
- data/app/controllers/scim_rails/application_controller.rb +72 -0
- data/app/controllers/scim_rails/scim_groups_controller.rb +96 -0
- data/app/controllers/scim_rails/scim_users_controller.rb +124 -0
- data/app/helpers/scim_rails/application_helper.rb +4 -0
- data/app/models/scim_rails/application_record.rb +5 -0
- data/app/models/scim_rails/authorize_api_request.rb +40 -0
- data/app/models/scim_rails/scim_count.rb +38 -0
- data/app/models/scim_rails/scim_query_parser.rb +47 -0
- data/config/initializers/mime_types.rb +5 -0
- data/config/routes.rb +12 -0
- data/lib/generators/scim_rails/USAGE +8 -0
- data/lib/generators/scim_rails/scim_rails_generator.rb +7 -0
- data/lib/generators/scim_rails/templates/initializer.rb +166 -0
- data/lib/scim_rails/config.rb +85 -0
- data/lib/scim_rails/encoder.rb +25 -0
- data/lib/scim_rails/engine.rb +12 -0
- data/lib/scim_rails/version.rb +5 -0
- data/lib/scim_rails.rb +6 -0
- data/lib/tasks/scim_rails_tasks.rake +4 -0
- data/spec/controllers/scim_rails/scim_groups_controller_spec.rb +494 -0
- data/spec/controllers/scim_rails/scim_groups_request_spec.rb +68 -0
- data/spec/controllers/scim_rails/scim_users_controller_spec.rb +681 -0
- data/spec/controllers/scim_rails/scim_users_request_spec.rb +77 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +5 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/javascripts/cable.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/company.rb +4 -0
- data/spec/dummy/app/models/group.rb +15 -0
- data/spec/dummy/app/models/group_user.rb +6 -0
- data/spec/dummy/app/models/user.rb +39 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +34 -0
- data/spec/dummy/bin/update +29 -0
- data/spec/dummy/config/application.rb +15 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +9 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +54 -0
- data/spec/dummy/config/environments/production.rb +86 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/new_framework_defaults.rb +24 -0
- data/spec/dummy/config/initializers/scim_rails_config.rb +85 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/puma.rb +47 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/db/migrate/20181206184304_create_users.rb +15 -0
- data/spec/dummy/db/migrate/20181206184313_create_companies.rb +11 -0
- data/spec/dummy/db/migrate/20210423075859_create_groups.rb +10 -0
- data/spec/dummy/db/migrate/20210423075950_create_group_users.rb +10 -0
- data/spec/dummy/db/schema.rb +53 -0
- data/spec/dummy/db/seeds.rb +14 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/company.rb +10 -0
- data/spec/factories/group.rb +11 -0
- data/spec/factories/user.rb +9 -0
- data/spec/lib/scim_rails/encoder_spec.rb +62 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/auth_helper.rb +7 -0
- data/spec/support/factory_bot.rb +3 -0
- data/spec/support/scim_rails_config.rb +59 -0
- metadata +339 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScimRails
|
|
4
|
+
class ScimGroupsController < ScimRails::ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
if params[:filter].present?
|
|
7
|
+
query = ScimRails::ScimQueryParser.new(
|
|
8
|
+
params[:filter], ScimRails.config.queryable_group_attributes
|
|
9
|
+
)
|
|
10
|
+
|
|
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)
|
|
18
|
+
else
|
|
19
|
+
groups = @company
|
|
20
|
+
.public_send(ScimRails.config.scim_groups_scope)
|
|
21
|
+
.preload(:users)
|
|
22
|
+
.order(ScimRails.config.scim_groups_list_order)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
counts = ScimCount.new(
|
|
26
|
+
start_index: params[:startIndex],
|
|
27
|
+
limit: params[:count],
|
|
28
|
+
total: groups.count
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
json_scim_response(object: groups, counts: counts)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def show
|
|
35
|
+
group = @company
|
|
36
|
+
.public_send(ScimRails.config.scim_groups_scope)
|
|
37
|
+
.find(params[:id])
|
|
38
|
+
json_scim_response(object: group)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create
|
|
42
|
+
group = @company
|
|
43
|
+
.public_send(ScimRails.config.scim_groups_scope)
|
|
44
|
+
.create!(permitted_group_params)
|
|
45
|
+
|
|
46
|
+
json_scim_response(object: group, status: :created)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def put_update
|
|
50
|
+
group = @company
|
|
51
|
+
.public_send(ScimRails.config.scim_groups_scope)
|
|
52
|
+
.find(params[:id])
|
|
53
|
+
group.update!(permitted_group_params)
|
|
54
|
+
json_scim_response(object: group)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def destroy
|
|
58
|
+
unless ScimRails.config.group_destroy_method
|
|
59
|
+
raise ScimRails::ExceptionHandler::UnsupportedDeleteRequest
|
|
60
|
+
end
|
|
61
|
+
group = @company
|
|
62
|
+
.public_send(ScimRails.config.scim_groups_scope)
|
|
63
|
+
.find(params[:id])
|
|
64
|
+
group.public_send(ScimRails.config.group_destroy_method)
|
|
65
|
+
head :no_content
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
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]
|
|
75
|
+
|
|
76
|
+
converted.merge(member_params)
|
|
77
|
+
end
|
|
78
|
+
|
|
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
|
|
87
|
+
|
|
88
|
+
def mutable_attributes
|
|
89
|
+
ScimRails.config.mutable_group_attributes
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def controller_schema
|
|
93
|
+
ScimRails.config.mutable_group_attributes_schema
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScimRails
|
|
4
|
+
class ScimUsersController < ScimRails::ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
if params[:filter].present?
|
|
7
|
+
query = ScimRails::ScimQueryParser.new(
|
|
8
|
+
params[:filter], ScimRails.config.queryable_user_attributes
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
users = @company
|
|
12
|
+
.public_send(ScimRails.config.scim_users_scope)
|
|
13
|
+
.where(
|
|
14
|
+
"#{ScimRails.config.scim_users_model.connection.quote_column_name(query.attribute)} #{query.operator} ?",
|
|
15
|
+
query.parameter
|
|
16
|
+
)
|
|
17
|
+
.order(ScimRails.config.scim_users_list_order)
|
|
18
|
+
else
|
|
19
|
+
users = @company
|
|
20
|
+
.public_send(ScimRails.config.scim_users_scope)
|
|
21
|
+
.order(ScimRails.config.scim_users_list_order)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
counts = ScimCount.new(
|
|
25
|
+
start_index: params[:startIndex],
|
|
26
|
+
limit: params[:count],
|
|
27
|
+
total: users.count
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
json_scim_response(object: users, counts: counts)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create
|
|
34
|
+
if ScimRails.config.scim_user_prevent_update_on_create
|
|
35
|
+
user = @company.public_send(ScimRails.config.scim_users_scope).create!(permitted_user_params)
|
|
36
|
+
else
|
|
37
|
+
username_key = ScimRails.config.queryable_user_attributes[:userName]
|
|
38
|
+
find_by_username = {}
|
|
39
|
+
find_by_username[username_key] = permitted_user_params[username_key]
|
|
40
|
+
user = @company
|
|
41
|
+
.public_send(ScimRails.config.scim_users_scope)
|
|
42
|
+
.find_or_create_by(find_by_username)
|
|
43
|
+
user.update!(permitted_user_params)
|
|
44
|
+
end
|
|
45
|
+
update_status(user) unless put_active_param.nil?
|
|
46
|
+
json_scim_response(object: user, status: :created)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def show
|
|
50
|
+
user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
|
|
51
|
+
json_scim_response(object: user)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def put_update
|
|
55
|
+
user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
|
|
56
|
+
update_status(user) unless put_active_param.nil?
|
|
57
|
+
user.update!(permitted_user_params)
|
|
58
|
+
json_scim_response(object: user)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# TODO: PATCH will only deprovision or reprovision users.
|
|
62
|
+
# This will work just fine for Okta but is not SCIM compliant.
|
|
63
|
+
def patch_update
|
|
64
|
+
user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
|
|
65
|
+
update_status(user)
|
|
66
|
+
json_scim_response(object: user)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def permitted_user_params
|
|
72
|
+
ScimRails.config.mutable_user_attributes.each.with_object({}) do |attribute, hash|
|
|
73
|
+
hash[attribute] = find_value_for(attribute)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def controller_schema
|
|
78
|
+
ScimRails.config.mutable_user_attributes_schema
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def update_status(user)
|
|
82
|
+
user.public_send(ScimRails.config.user_reprovision_method) if active?
|
|
83
|
+
user.public_send(ScimRails.config.user_deprovision_method) unless active?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def active?
|
|
87
|
+
active = put_active_param
|
|
88
|
+
active = patch_active_param if active.nil?
|
|
89
|
+
|
|
90
|
+
case active
|
|
91
|
+
when true, "true", 1
|
|
92
|
+
true
|
|
93
|
+
when false, "false", 0
|
|
94
|
+
false
|
|
95
|
+
else
|
|
96
|
+
raise ActiveRecord::RecordInvalid
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def put_active_param
|
|
101
|
+
params[:active]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def patch_active_param
|
|
105
|
+
handle_invalid = lambda do
|
|
106
|
+
raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
operations = params["Operations"] || {}
|
|
110
|
+
|
|
111
|
+
valid_operation = operations.find(handle_invalid) do |operation|
|
|
112
|
+
valid_patch_operation?(operation)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
valid_operation.dig("value", "active")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def valid_patch_operation?(operation)
|
|
119
|
+
operation["op"].casecmp("replace") &&
|
|
120
|
+
operation["value"] &&
|
|
121
|
+
[true, false].include?(operation["value"]["active"])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module ScimRails
|
|
2
|
+
class AuthorizeApiRequest
|
|
3
|
+
|
|
4
|
+
def initialize(searchable_attribute:, authentication_attribute:)
|
|
5
|
+
@searchable_attribute = searchable_attribute
|
|
6
|
+
@authentication_attribute = authentication_attribute
|
|
7
|
+
|
|
8
|
+
raise ScimRails::ExceptionHandler::InvalidCredentials if searchable_attribute.blank? || authentication_attribute.blank?
|
|
9
|
+
|
|
10
|
+
@search_parameter = { ScimRails.config.basic_auth_model_searchable_attribute => @searchable_attribute }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def company
|
|
14
|
+
company = find_company
|
|
15
|
+
authorize(company)
|
|
16
|
+
company
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :authentication_attribute
|
|
22
|
+
attr_reader :search_parameter
|
|
23
|
+
attr_reader :searchable_attribute
|
|
24
|
+
|
|
25
|
+
def find_company
|
|
26
|
+
@company ||= ScimRails.config.basic_auth_model.find_by!(search_parameter)
|
|
27
|
+
|
|
28
|
+
rescue ActiveRecord::RecordNotFound
|
|
29
|
+
raise ScimRails::ExceptionHandler::InvalidCredentials
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def authorize(authentication_model)
|
|
33
|
+
authorized = ActiveSupport::SecurityUtils.secure_compare(
|
|
34
|
+
authentication_model.public_send(ScimRails.config.basic_auth_model_authenticatable_attribute),
|
|
35
|
+
authentication_attribute
|
|
36
|
+
)
|
|
37
|
+
raise ScimRails::ExceptionHandler::InvalidCredentials unless authorized
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module ScimRails
|
|
2
|
+
class ScimCount
|
|
3
|
+
include ActiveModel::Model
|
|
4
|
+
|
|
5
|
+
attr_accessor \
|
|
6
|
+
:limit,
|
|
7
|
+
:offset,
|
|
8
|
+
:start_index,
|
|
9
|
+
:total
|
|
10
|
+
|
|
11
|
+
def limit
|
|
12
|
+
return 100 if @limit.blank?
|
|
13
|
+
validate_numericality(@limit)
|
|
14
|
+
input = @limit.to_i
|
|
15
|
+
raise if input < 1
|
|
16
|
+
input
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start_index
|
|
20
|
+
return 1 if @start_index.blank?
|
|
21
|
+
validate_numericality(@start_index)
|
|
22
|
+
input = @start_index.to_i
|
|
23
|
+
return 1 if input < 1
|
|
24
|
+
input
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def offset
|
|
28
|
+
start_index - 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def validate_numericality(input)
|
|
34
|
+
raise unless input.match?(/\A\d+\z/)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScimRails
|
|
4
|
+
class ScimQueryParser
|
|
5
|
+
attr_accessor :query_elements, :query_attributes
|
|
6
|
+
|
|
7
|
+
def initialize(query_string, queryable_attributes)
|
|
8
|
+
self.query_elements = query_string.split
|
|
9
|
+
self.query_attributes = queryable_attributes
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def attribute
|
|
13
|
+
attribute = query_elements[0]
|
|
14
|
+
raise ScimRails::ExceptionHandler::InvalidQuery if attribute.blank?
|
|
15
|
+
|
|
16
|
+
attribute = attribute.to_sym
|
|
17
|
+
|
|
18
|
+
mapped_attribute = query_attributes[attribute]
|
|
19
|
+
raise ScimRails::ExceptionHandler::InvalidQuery if mapped_attribute.blank?
|
|
20
|
+
|
|
21
|
+
mapped_attribute
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def operator
|
|
25
|
+
sql_comparison_operator(query_elements[1])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parameter
|
|
29
|
+
parameter = query_elements[2..-1].join(" ")
|
|
30
|
+
return if parameter.blank?
|
|
31
|
+
|
|
32
|
+
parameter.gsub(/"/, "")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def sql_comparison_operator(element)
|
|
38
|
+
case element
|
|
39
|
+
when "eq"
|
|
40
|
+
"="
|
|
41
|
+
else
|
|
42
|
+
# TODO: implement additional query filters
|
|
43
|
+
raise ScimRails::ExceptionHandler::InvalidQuery
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
ScimRails::Engine.routes.draw do
|
|
2
|
+
get 'scim/v2/Users', action: :index, controller: 'scim_users'
|
|
3
|
+
post 'scim/v2/Users', action: :create, controller: 'scim_users'
|
|
4
|
+
get 'scim/v2/Users/:id', action: :show, controller: 'scim_users'
|
|
5
|
+
put 'scim/v2/Users/:id', action: :put_update, controller: 'scim_users'
|
|
6
|
+
patch 'scim/v2/Users/:id', action: :patch_update, controller: 'scim_users'
|
|
7
|
+
get 'scim/v2/Groups', action: :index, controller: 'scim_groups'
|
|
8
|
+
post 'scim/v2/Groups', action: :create, controller: 'scim_groups'
|
|
9
|
+
get 'scim/v2/Groups/:id', action: :show, controller: 'scim_groups'
|
|
10
|
+
put 'scim/v2/Groups/:id', action: :put_update, controller: 'scim_groups'
|
|
11
|
+
delete 'scim/v2/Groups/:id', action: :destroy, controller: 'scim_groups'
|
|
12
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ScimRails.configure do |config|
|
|
4
|
+
# Model used for authenticating and scoping users.
|
|
5
|
+
config.basic_auth_model = "Company"
|
|
6
|
+
|
|
7
|
+
# Attribute used to search for a given record. This
|
|
8
|
+
# attribute should be unique as it will return the
|
|
9
|
+
# first found record.
|
|
10
|
+
config.basic_auth_model_searchable_attribute = :subdomain
|
|
11
|
+
|
|
12
|
+
# Attribute used to compare Basic Auth password value.
|
|
13
|
+
# Attribute will need to return plaintext for comparison.
|
|
14
|
+
config.basic_auth_model_authenticatable_attribute = :api_token
|
|
15
|
+
|
|
16
|
+
# Model used for user records.
|
|
17
|
+
config.scim_users_model = "User"
|
|
18
|
+
|
|
19
|
+
# Method used for retrieving user records from the
|
|
20
|
+
# authenticatable model.
|
|
21
|
+
config.scim_users_scope = :users
|
|
22
|
+
|
|
23
|
+
# Determine whether the create endpoint updates users that already exist
|
|
24
|
+
# or throws an error (returning 409 Conflict in accordance with SCIM spec)
|
|
25
|
+
config.scim_user_prevent_update_on_create = false
|
|
26
|
+
|
|
27
|
+
# Model used for group records.
|
|
28
|
+
config.scim_groups_model = "Group"
|
|
29
|
+
# Method used for retrieving user records from the
|
|
30
|
+
# authenticatable model.
|
|
31
|
+
config.scim_groups_scope = :groups
|
|
32
|
+
|
|
33
|
+
# Cryptographic algorithm used for signing the auth tokens.
|
|
34
|
+
# It supports all algorithms supported by the jwt gem.
|
|
35
|
+
# See https://github.com/jwt/ruby-jwt#algorithms-and-usage for supported algorithms
|
|
36
|
+
# It is "none" by default, hence generated tokens are unsigned
|
|
37
|
+
# The tokens do not need to be signed if you only need basic authentication.
|
|
38
|
+
# config.signing_algorithm = "HS256"
|
|
39
|
+
|
|
40
|
+
# Secret token used to sign authorization tokens
|
|
41
|
+
# It is `nil` by default, hence generated tokens are unsigned
|
|
42
|
+
# The tokens do not need to be signed if you only need basic authentication.
|
|
43
|
+
# config.signing_secret = SECRET_TOKEN
|
|
44
|
+
|
|
45
|
+
# Default sort order for pagination is by id. If you
|
|
46
|
+
# use non sequential ids for user records, uncomment
|
|
47
|
+
# the below line and configure a determinate order.
|
|
48
|
+
# For example, [:created_at, :id] or { created_at: :desc }.
|
|
49
|
+
# config.scim_users_list_order = :id
|
|
50
|
+
|
|
51
|
+
# Method called on user model to deprovision a user.
|
|
52
|
+
config.user_deprovision_method = :archive!
|
|
53
|
+
|
|
54
|
+
# Method called on user model to reprovision a user.
|
|
55
|
+
config.user_reprovision_method = :unarchive!
|
|
56
|
+
|
|
57
|
+
# Hash of queryable attribtues on the user model. If
|
|
58
|
+
# the attribute is not listed in this hash it cannot
|
|
59
|
+
# be queried by this Gem. The structure of this hash
|
|
60
|
+
# is { queryable_scim_attribute => user_attribute }.
|
|
61
|
+
config.queryable_user_attributes = {
|
|
62
|
+
userName: :email,
|
|
63
|
+
givenName: :first_name,
|
|
64
|
+
familyName: :last_name,
|
|
65
|
+
email: :email
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Array of attributes that can be modified on the
|
|
69
|
+
# user model. If the attribute is not in this array
|
|
70
|
+
# the attribute cannot be modified by this Gem.
|
|
71
|
+
config.mutable_user_attributes = [
|
|
72
|
+
:first_name,
|
|
73
|
+
:last_name,
|
|
74
|
+
:email
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# Hash of mutable attributes. This object is the map
|
|
78
|
+
# for this Gem to figure out where to look in a SCIM
|
|
79
|
+
# response for mutable values. This object should
|
|
80
|
+
# include all attributes listed in
|
|
81
|
+
# config.mutable_user_attributes.
|
|
82
|
+
config.mutable_user_attributes_schema = {
|
|
83
|
+
name: {
|
|
84
|
+
givenName: :first_name,
|
|
85
|
+
familyName: :last_name
|
|
86
|
+
},
|
|
87
|
+
emails: [
|
|
88
|
+
{
|
|
89
|
+
value: :email
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Hash of SCIM structure for a user schema. This object
|
|
95
|
+
# is what will be returned for a given user. The keys
|
|
96
|
+
# in this object should conform to standard SCIM
|
|
97
|
+
# structures. The values in the object will be
|
|
98
|
+
# transformed per user record. Strings will be passed
|
|
99
|
+
# through as is, symbols will be passed to the user
|
|
100
|
+
# object to return a value.
|
|
101
|
+
config.user_schema = {
|
|
102
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
|
103
|
+
id: :id,
|
|
104
|
+
userName: :email,
|
|
105
|
+
name: {
|
|
106
|
+
givenName: :first_name,
|
|
107
|
+
familyName: :last_name
|
|
108
|
+
},
|
|
109
|
+
emails: [
|
|
110
|
+
{
|
|
111
|
+
value: :email
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
active: :active?
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Schema for users used in "abbreviated" lists such as in
|
|
118
|
+
# the `members` field of a Group.
|
|
119
|
+
config.user_abbreviated_schema = {
|
|
120
|
+
value: :id,
|
|
121
|
+
display: :email
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Allow filtering Groups based on these parameters
|
|
125
|
+
config.queryable_group_attributes = {
|
|
126
|
+
displayName: :name
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# List of attributes on a Group that can be updated through SCIM
|
|
130
|
+
config.mutable_group_attributes = [
|
|
131
|
+
:name
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
# Hash of mutable Group attributes. This object is the map
|
|
135
|
+
# for this Gem to figure out where to look in a SCIM
|
|
136
|
+
# response for mutable values. This object should
|
|
137
|
+
# include all attributes listed in
|
|
138
|
+
# config.mutable_group_attributes.
|
|
139
|
+
config.mutable_group_attributes_schema = {
|
|
140
|
+
displayName: :name
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# The User relation's IDs field name on the Group model.
|
|
144
|
+
# Eg. if the relation is `has_many :users` this will be :user_ids
|
|
145
|
+
config.group_member_relation_attribute = :user_ids
|
|
146
|
+
# Which fields from the request's `members` field should be
|
|
147
|
+
# assigned to the relation IDs field. Should include the field
|
|
148
|
+
# set in config.group_member_relation_attribute.
|
|
149
|
+
config.group_member_relation_schema = { value: :user_ids }
|
|
150
|
+
|
|
151
|
+
config.group_schema = {
|
|
152
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
|
153
|
+
id: :id,
|
|
154
|
+
displayName: :name,
|
|
155
|
+
members: :users
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
config.group_abbreviated_schema = {
|
|
159
|
+
value: :id,
|
|
160
|
+
display: :name
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Set group_destroy_method to a method on the Group model
|
|
164
|
+
# to be called on a destroy request
|
|
165
|
+
# config.group_destroy_method = :destroy!
|
|
166
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScimRails
|
|
4
|
+
class << self
|
|
5
|
+
def configure
|
|
6
|
+
yield config
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def config
|
|
10
|
+
@config ||= Config.new
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Class containing configuration of ScimRails
|
|
15
|
+
class Config
|
|
16
|
+
ALGO_NONE = "none"
|
|
17
|
+
|
|
18
|
+
attr_writer \
|
|
19
|
+
:basic_auth_model,
|
|
20
|
+
:mutable_user_attributes_schema,
|
|
21
|
+
:mutable_group_attributes_schema,
|
|
22
|
+
:scim_users_model,
|
|
23
|
+
:scim_groups_model
|
|
24
|
+
|
|
25
|
+
attr_accessor \
|
|
26
|
+
:basic_auth_model_authenticatable_attribute,
|
|
27
|
+
:basic_auth_model_searchable_attribute,
|
|
28
|
+
:mutable_user_attributes,
|
|
29
|
+
:on_error,
|
|
30
|
+
:queryable_user_attributes,
|
|
31
|
+
:queryable_group_attributes,
|
|
32
|
+
:scim_users_list_order,
|
|
33
|
+
:scim_users_scope,
|
|
34
|
+
:scim_user_prevent_update_on_create,
|
|
35
|
+
:mutable_group_attributes,
|
|
36
|
+
:scim_groups_list_order,
|
|
37
|
+
:scim_groups_scope,
|
|
38
|
+
:group_member_relation_attribute,
|
|
39
|
+
:group_member_relation_schema,
|
|
40
|
+
:user_abbreviated_schema,
|
|
41
|
+
:group_abbreviated_schema,
|
|
42
|
+
:signing_secret,
|
|
43
|
+
:signing_algorithm,
|
|
44
|
+
:user_attributes,
|
|
45
|
+
:user_deprovision_method,
|
|
46
|
+
:user_reprovision_method,
|
|
47
|
+
:user_schema,
|
|
48
|
+
:group_schema,
|
|
49
|
+
:group_destroy_method
|
|
50
|
+
|
|
51
|
+
def initialize
|
|
52
|
+
@basic_auth_model = "Company"
|
|
53
|
+
@scim_users_list_order = :id
|
|
54
|
+
@scim_users_model = "User"
|
|
55
|
+
@scim_groups_list_order = :id
|
|
56
|
+
@scim_groups_model = "Group"
|
|
57
|
+
@signing_algorithm = ALGO_NONE
|
|
58
|
+
@user_schema = {}
|
|
59
|
+
@user_attributes = []
|
|
60
|
+
@user_abbreviated_schema = {}
|
|
61
|
+
@group_schema = {}
|
|
62
|
+
@group_abbreviated_schema = {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def mutable_user_attributes_schema
|
|
66
|
+
@mutable_user_attributes_schema || @user_schema
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def mutable_group_attributes_schema
|
|
70
|
+
@mutable_group_attributes_schema || @group_schema
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def basic_auth_model
|
|
74
|
+
@basic_auth_model.constantize
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def scim_users_model
|
|
78
|
+
@scim_users_model.constantize
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def scim_groups_model
|
|
82
|
+
@scim_groups_model.constantize
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "jwt"
|
|
2
|
+
|
|
3
|
+
module ScimRails
|
|
4
|
+
module Encoder
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def encode(company)
|
|
8
|
+
payload = {
|
|
9
|
+
iat: Time.current.to_i,
|
|
10
|
+
ScimRails.config.basic_auth_model_searchable_attribute =>
|
|
11
|
+
company.public_send(ScimRails.config.basic_auth_model_searchable_attribute)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
JWT.encode(payload, ScimRails.config.signing_secret, ScimRails.config.signing_algorithm)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def decode(token)
|
|
18
|
+
verify = ScimRails.config.signing_algorithm != ScimRails::Config::ALGO_NONE
|
|
19
|
+
|
|
20
|
+
JWT.decode(token, ScimRails.config.signing_secret, verify, algorithm: ScimRails.config.signing_algorithm).first
|
|
21
|
+
rescue JWT::VerificationError, JWT::DecodeError
|
|
22
|
+
raise ScimRails::ExceptionHandler::InvalidCredentials
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module ScimRails
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace ScimRails
|
|
4
|
+
|
|
5
|
+
config.generators do |g|
|
|
6
|
+
g.test_framework :rspec, :fixture => false
|
|
7
|
+
g.fixture_replacement :factory_bot, :dir => "spec/factories"
|
|
8
|
+
g.assets false
|
|
9
|
+
g.helper false
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|