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
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
51 changes: 34 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,28 @@ 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 +70,28 @@ 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)
value = object.public_send(schema)
case value
when true, false, String, Integer, DateTime then value
else find_value(object, value)
end
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
79 changes: 79 additions & 0 deletions app/controllers/scim_rails/scim_groups_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 = ScimRails.config.mutable_group_attributes.each.with_object({}) do |attribute, hash|
hash[attribute] = find_value_for(attribute)
end
return converted unless params[:members]
converted.merge(
ScimRails.config.group_member_relation_attribute =>
params[:members].map do |member|
member[ScimRails.config.group_member_relation_schema.keys.first]
end
)
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
56 changes: 56 additions & 0 deletions lib/generators/scim_rails/templates/initializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
# or throws an error (returning 409 Conflict in accordance with SCIM spec)
config.scim_user_prevent_update_on_create = false

# Model used for group records.
config.scim_groups_model = 'Group'
# Method used for retrieving user records from the
# authenticatable model.
config.scim_groups_scope = :groups

# Cryptographic algorithm used for signing the auth tokens.
# It supports all algorithms supported by the jwt gem.
# See https://github.com/jwt/ruby-jwt#algorithms-and-usage for supported algorithms
Expand Down Expand Up @@ -105,4 +111,54 @@
],
active: :active?
}

# Schema for users used in "abbreviated" lists such as in
# the `members` field of a Group.
config.user_abbreviated_schema = {
value: :id,
display: :email
}

# Allow filtering Groups based on these parameters
config.queryable_group_attributes = {
displayName: :name
}

# List of attributes on a Group that can be updated through SCIM
config.mutable_group_attributes = [
:name
]

# Hash of mutable Group attributes. This object is the map
# for this Gem to figure out where to look in a SCIM
# response for mutable values. This object should
# include all attributes listed in
# config.mutable_group_attributes.
config.mutable_group_attributes_schema = {
displayName: :name
}

# The User relation's IDs field name on the Group model.
# Eg. if the relation is `has_many :users` this will be :user_ids
config.group_member_relation_attribute = :user_ids
# Which fields from the request's `members` field should be
# assigned to the relation IDs field. Should include the field
# set in config.group_member_relation_attribute.
config.group_member_relation_schema = { value: :user_ids }

config.group_schema = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
id: :id,
displayName: :name,
members: :users
}

config.group_abbreviated_schema = {
value: :id,
display: :name
}

# Set group_destroy_method to a method on the Group model
# to be called on a destroy request
# config.group_destroy_method = :destroy!
end
Loading