Skip to content

Commit

Permalink
Merge pull request #1 from valerauko/groups
Browse files Browse the repository at this point in the history
Why?
Support for SCIM user Groups is currently missing
lessonly#46

What?
add options to configure how to handle Groups
add API endpoints to deal with Groups
  • Loading branch information
mtakeda15 authored Nov 19, 2021
2 parents fe8caaf + f3425e2 commit 79a9da4
Show file tree
Hide file tree
Showing 23 changed files with 1,021 additions and 109 deletions.
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)
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 = [])
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
def index
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} ?",
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

0 comments on commit 79a9da4

Please sign in to comment.