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 #1

Merged
merged 14 commits into from
Nov 19, 2021
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),

Choose a reason for hiding this comment

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

response.rb の修正:userしか扱わない前提だったが、groupも共通で扱えるようにするために 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)

Choose a reason for hiding this comment

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

list_objects -> object_response -> find_value と関数が呼ばれていくが、
これらは GET(index) の処理で利用しているもの。

}
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)
Comment on lines 75 to +77

Choose a reason for hiding this comment

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

一番最初のfind_valueは絶対にここに入る
(ScimRails.config.user_schema or ScimRails.config.group_schema の2択)

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)
Comment on lines +83 to +84

Choose a reason for hiding this comment

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

Group のレスポンスを返す時に、members としてユーザの情報を返す。
そこに含まれるユーザ情報は通常と違い省略された値が入る。
そのため、members の中身に対応するハッシュを取得する時はコンフィグの user_abbreviated_schema に従って値を取得する。

   {
     "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
     "id": "e9e30dba-f08f-4109-8486-d5c6a331660a",
     "displayName": "Tour Guides",
     "members": [
       {
         "value": "2819c223-7f76-453a-919d-413861904646",
         "$ref":
   "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
         "display": "Babs Jensen"
       },
       {
         "value": "902c246b-6245-4190-8e05-00816be7344a",
         "$ref":
   "https://example.com/v2/Users/902c246b-6245-4190-8e05-00816be7344a",
         "display": "Mandy Pepperidge"
       }
     ],
     "meta": {
       "resourceType": "Group",
       "created": "2010-01-23T04:56:22Z",
       "lastModified": "2011-05-13T04:42:34Z",
       "version": "W\/\"3694e05e9dff592\"",
       "location":
   "https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a"
     }
   }

when ScimRails.config.scim_groups_model
find_value(schema, ScimRails.config.group_abbreviated_schema)
Comment on lines +85 to +86

Choose a reason for hiding this comment

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

上述の Group に対する members のように、Userのレスポンスを返す時に groups として情報を返すパターンで利用。

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

Choose a reason for hiding this comment

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

find_value_for および、path_for は scim_users_controller.rb に存在したが、Groupでも使うので application_controller に移動。


# `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