Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Groups support #48

Closed
wants to merge 14 commits into from
6 changes: 6 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
inherit_from:
- https://raw.githubusercontent.com/lessonly/rubocop-default-configuration/master/.rubocop.yml

Metrics/BlockLength:
# don't warn about block length in block-centered DSLs
Exclude:
- 'config/routes.rb'
- 'spec/**/*.rb'
14 changes: 14 additions & 0 deletions app/controllers/concerns/scim_rails/exception_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class InvalidQuery < StandardError
class UnsupportedPatchRequest < StandardError
end

class UnsupportedDeleteRequest < StandardError
end

included do
if Rails.env.production?
rescue_from StandardError do |exception|
Expand Down Expand Up @@ -65,6 +68,17 @@ class UnsupportedPatchRequest < StandardError
)
end

rescue_from ScimRails::ExceptionHandler::UnsupportedDeleteRequest do
json_response(
{
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
detail: "Delete operation is disabled for the requested resource.",
status: "501"
},
:not_implemented
)
end

rescue_from ActiveRecord::RecordNotFound do |e|
json_response(
{
Expand Down
46 changes: 29 additions & 17 deletions app/controllers/concerns/scim_rails/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def json_scim_response(object:, status: :ok, counts: nil)
content_type: CONTENT_TYPE
when "show", "create", "put_update", "patch_update"
render \
json: user_response(object),
json: object_response(object),
status: status,
content_type: CONTENT_TYPE
end
Expand All @@ -38,19 +38,27 @@ def list_response(object, counts)
"totalResults": counts.total,
"startIndex": counts.start_index,
"itemsPerPage": counts.limit,
"Resources": list_users(object)
"Resources": list_objects(object)
}
end

def list_users(users)
users.map do |user|
user_response(user)
def list_objects(objects)
objects.map do |object|
object_response(object)
end
end

def user_response(user)
schema = ScimRails.config.user_schema
find_value(user, schema)
def object_response(object)
schema = case object
when ScimRails.config.scim_users_model
ScimRails.config.user_schema
when ScimRails.config.scim_groups_model
ScimRails.config.group_schema
else
raise ScimRails::ExceptionHandler::InvalidQuery,
"Unknown model: #{object}"
end
find_value(object, schema)
end


Expand All @@ -61,20 +69,24 @@ def user_response(user)
# send those symbols to the model, and replace the symbol with
# the return value.

def find_value(user, object)
case object
def find_value(object, schema)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/AbcSize: Assignment Branch Condition size for find_value is too high. [17.75/15]
Metrics/MethodLength: Method has too many lines. [18/10]

case schema
when Hash
object.each.with_object({}) do |(key, value), hash|
hash[key] = find_value(user, value)
schema.each.with_object({}) do |(key, value), hash|
hash[key] = find_value(object, value)
end
when Array
object.map do |value|
find_value(user, value)
when Array, ActiveRecord::Associations::CollectionProxy
schema.map do |value|
find_value(object, value)
end
when ScimRails.config.scim_users_model
find_value(schema, ScimRails.config.user_abbreviated_schema)
when ScimRails.config.scim_groups_model
find_value(schema, ScimRails.config.group_abbreviated_schema)
when Symbol
user.public_send(object)
find_value(object, object.public_send(schema))
else
object
schema
end
end
end
Expand Down
32 changes: 32 additions & 0 deletions app/controllers/scim_rails/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,37 @@ def authenticate_with_oauth_bearer

yield searchable_attribute, authentication_attribute
end

def find_value_for(attribute)
params.dig(*path_for(attribute))
end

# `path_for` is a recursive method used to find the "path" for
# `.dig` to take when looking for a given attribute in the
# params.
#
# Example: `path_for(:name)` should return an array that looks
# like [:names, 0, :givenName]. `.dig` can then use that path
# against the params to translate the :name attribute to "John".

def path_for(attribute, object = controller_schema, path = [])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/CyclomaticComplexity: Cyclomatic complexity for path_for is too high. [7/6]
Metrics/MethodLength: Method has too many lines. [16/10]

at_path = path.empty? ? object : object.dig(*path)
return path if at_path == attribute

case at_path
when Hash
at_path.each do |key, _value|
found_path = path_for(attribute, object, [*path, key])
return found_path if found_path
end
nil
when Array
at_path.each_with_index do |_value, index|
found_path = path_for(attribute, object, [*path, index])
return found_path if found_path
end
nil
end
end
end
end
93 changes: 93 additions & 0 deletions app/controllers/scim_rails/scim_groups_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
module ScimRails
class ScimGroupsController < ScimRails::ApplicationController
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/Documentation: Missing top-level class documentation comment.

def index
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/AbcSize: Assignment Branch Condition size for index is too high. [36.24/15]
Metrics/MethodLength: Method has too many lines. [23/10]

if params[:filter].present?
query = ScimRails::ScimQueryParser.new(
params[:filter], ScimRails.config.queryable_group_attributes
)

groups = @company
.public_send(ScimRails.config.scim_groups_scope)
.where(
"#{ScimRails.config.scim_groups_model.connection.quote_column_name(query.attribute)} #{query.operator} ?",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [118/80]

query.parameter
)
.order(ScimRails.config.scim_groups_list_order)
else
groups = @company
.public_send(ScimRails.config.scim_groups_scope)
.preload(:users)
.order(ScimRails.config.scim_groups_list_order)
end

counts = ScimCount.new(
start_index: params[:startIndex],
limit: params[:count],
total: groups.count
)

json_scim_response(object: groups, counts: counts)
end

def show
group = @company
.public_send(ScimRails.config.scim_groups_scope)
.find(params[:id])
json_scim_response(object: group)
end

def create
group = @company
.public_send(ScimRails.config.scim_groups_scope)
.create!(permitted_group_params)

json_scim_response(object: group, status: :created)
end

def put_update
group = @company
.public_send(ScimRails.config.scim_groups_scope)
.find(params[:id])
group.update!(permitted_group_params)
json_scim_response(object: group)
end

def destroy
unless ScimRails.config.group_destroy_method
raise ScimRails::ExceptionHandler::UnsupportedDeleteRequest
end
group = @company
.public_send(ScimRails.config.scim_groups_scope)
.find(params[:id])
group.public_send(ScimRails.config.group_destroy_method)
head :no_content
end

private

def permitted_group_params
converted = mutable_attributes.each.with_object({}) do |attribute, hash|
hash[attribute] = find_value_for(attribute)
end
return converted unless params[:members]
converted.merge(member_params)
end

def member_params
{
ScimRails.config.group_member_relation_attribute =>
params[:members].map do |member|
member[ScimRails.config.group_member_relation_schema.keys.first]
end
}
end

def mutable_attributes
ScimRails.config.mutable_group_attributes
end

def controller_schema
ScimRails.config.mutable_group_attributes_schema
end
end
end
36 changes: 5 additions & 31 deletions app/controllers/scim_rails/scim_users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ module ScimRails
class ScimUsersController < ScimRails::ApplicationController
def index
if params[:filter].present?
query = ScimRails::ScimQueryParser.new(params[:filter])
query = ScimRails::ScimQueryParser.new(
params[:filter], ScimRails.config.queryable_user_attributes
)

users = @company
.public_send(ScimRails.config.scim_users_scope)
Expand Down Expand Up @@ -70,36 +72,8 @@ def permitted_user_params
end
end

def find_value_for(attribute)
params.dig(*path_for(attribute))
end

# `path_for` is a recursive method used to find the "path" for
# `.dig` to take when looking for a given attribute in the
# params.
#
# Example: `path_for(:name)` should return an array that looks
# like [:names, 0, :givenName]. `.dig` can then use that path
# against the params to translate the :name attribute to "John".

def path_for(attribute, object = ScimRails.config.mutable_user_attributes_schema, path = [])
at_path = path.empty? ? object : object.dig(*path)
return path if at_path == attribute

case at_path
when Hash
at_path.each do |key, value|
found_path = path_for(attribute, object, [*path, key])
return found_path if found_path
end
nil
when Array
at_path.each_with_index do |value, index|
found_path = path_for(attribute, object, [*path, index])
return found_path if found_path
end
nil
end
def controller_schema
ScimRails.config.mutable_user_attributes_schema
end

def update_status(user)
Expand Down
11 changes: 4 additions & 7 deletions app/models/scim_rails/scim_query_parser.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
module ScimRails
class ScimQueryParser
attr_accessor :query_elements
attr_accessor :query_elements, :query_attributes

def initialize(query_string)
def initialize(query_string, queryable_attributes)
self.query_elements = query_string.split(" ")
self.query_attributes = queryable_attributes
end

def attribute
attribute = query_elements.dig(0)
raise ScimRails::ExceptionHandler::InvalidQuery if attribute.blank?
attribute = attribute.to_sym

mapped_attribute = attribute_mapping(attribute)
mapped_attribute = query_attributes[attribute]
raise ScimRails::ExceptionHandler::InvalidQuery if mapped_attribute.blank?
mapped_attribute
end
Expand All @@ -28,10 +29,6 @@ def parameter

private

def attribute_mapping(attribute)
ScimRails.config.queryable_user_attributes[attribute]
end

def sql_comparison_operator(element)
case element
when "eq"
Expand Down
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@
get 'scim/v2/Users/:id', action: :show, controller: 'scim_users'
put 'scim/v2/Users/:id', action: :put_update, controller: 'scim_users'
patch 'scim/v2/Users/:id', action: :patch_update, controller: 'scim_users'
get 'scim/v2/Groups', action: :index, controller: 'scim_groups'
post 'scim/v2/Groups', action: :create, controller: 'scim_groups'
get 'scim/v2/Groups/:id', action: :show, controller: 'scim_groups'
put 'scim/v2/Groups/:id', action: :put_update, controller: 'scim_groups'
delete 'scim/v2/Groups/:id', action: :destroy, controller: 'scim_groups'
end
Loading