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
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
66 changes: 66 additions & 0 deletions app/controllers/scim_rails/scim_groups_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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])

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

private

def permitted_group_params
ScimRails.config.mutable_group_attributes.each.with_object({}) do |attribute, hash|
hash[attribute] = find_value_for(attribute)
end.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
32 changes: 2 additions & 30 deletions app/controllers/scim_rails/scim_users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,36 +70,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
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'

end
47 changes: 47 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,45 @@
],
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
}

# 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
}
end
25 changes: 24 additions & 1 deletion lib/scim_rails/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Config
attr_writer \
:basic_auth_model,
:mutable_user_attributes_schema,
:mutable_group_attributes_schema,
:scim_users_model

attr_accessor \
Expand All @@ -29,32 +30,54 @@ class Config
:scim_users_list_order,
:scim_users_scope,
:scim_user_prevent_update_on_create,
:mutable_group_attributes,
:scim_groups_list_order,
:scim_groups_model,
:scim_groups_scope,
:group_member_relation_attribute,
:group_member_relation_schema,
:user_abbreviated_schema,
:group_abbreviated_schema,
:signing_secret,
:signing_algorithm,
:user_attributes,
:user_deprovision_method,
:user_reprovision_method,
:user_schema
:user_schema,
:group_schema

def initialize
@basic_auth_model = "Company"
@scim_users_list_order = :id
@scim_users_model = "User"
@scim_groups_list_order = :id
@scim_groups_model = 'Group'
@signing_algorithm = ALGO_NONE
@user_schema = {}
@user_attributes = []
@user_abbreviated_schema = {}
@group_schema = {}
@group_abbreviated_schema = {}
end

def mutable_user_attributes_schema
@mutable_user_attributes_schema || @user_schema
end

def mutable_group_attributes_schema
@mutable_group_attributes_schema || @group_schema
end

def basic_auth_model
@basic_auth_model.constantize
end

def scim_users_model
@scim_users_model.constantize
end

def scim_groups_model
@scim_groups_model.constantize
end
end
end