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'
16 changes: 16 additions & 0 deletions app/controllers/concerns/scim_rails/exception_handler.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

module ScimRails
module ExceptionHandler
extend ActiveSupport::Concern
Expand All @@ -11,6 +13,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 +70,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
61 changes: 37 additions & 24 deletions app/controllers/concerns/scim_rails/response.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

module ScimRails
module Response
CONTENT_TYPE = "application/scim+json".freeze
CONTENT_TYPE = "application/scim+json"

def json_response(object, status = :ok)
render \
Expand All @@ -18,7 +20,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 @@ -32,49 +34,60 @@ def list_response(object, counts)
.offset(counts.offset)
.limit(counts.limit)
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
schemas: [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": counts.total,
"startIndex": counts.start_index,
"itemsPerPage": counts.limit,
"Resources": list_users(object)
totalResults: counts.total,
startIndex: counts.start_index,
itemsPerPage: counts.limit,
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


# `find_value` is a recursive method that takes a "user" and a
# "user schema" and replaces any symbols in the schema with the
# corresponding value from the user. Given a schema with symbols,
# `find_value` will search through the object for the symbols,
# 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
36 changes: 35 additions & 1 deletion app/controllers/scim_rails/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

module ScimRails
class ApplicationController < ActionController::API
include ActionController::HttpAuthentication::Basic::ControllerMethods
Expand Down Expand Up @@ -28,11 +30,43 @@ def authentication_strategy
end

def authenticate_with_oauth_bearer
authentication_attribute = request.headers["Authorization"].split(" ").last
authentication_attribute = request.headers["Authorization"].split.last
payload = ScimRails::Encoder.decode(authentication_attribute).with_indifferent_access
searchable_attribute = payload[ScimRails.config.basic_auth_model_searchable_attribute]

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

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
40 changes: 8 additions & 32 deletions app/controllers/scim_rails/scim_users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# frozen_string_literal: true

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 @@ -31,7 +35,7 @@ def create
user = @company.public_send(ScimRails.config.scim_users_scope).create!(permitted_user_params)
else
username_key = ScimRails.config.queryable_user_attributes[:userName]
find_by_username = Hash.new
find_by_username = {}
find_by_username[username_key] = permitted_user_params[username_key]
user = @company
.public_send(ScimRails.config.scim_users_scope)
Expand Down Expand Up @@ -70,36 +74,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
Loading