scimaenaga 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -8
  3. data/Rakefile +6 -8
  4. data/app/controllers/concerns/{scim_rails → scimaenaga}/exception_handler.rb +10 -10
  5. data/app/controllers/concerns/scimaenaga/response.rb +94 -0
  6. data/app/controllers/scimaenaga/application_controller.rb +72 -0
  7. data/app/controllers/{scim_rails → scimaenaga}/scim_groups_controller.rb +25 -25
  8. data/app/controllers/{scim_rails → scimaenaga}/scim_schemas_controller.rb +5 -5
  9. data/app/controllers/scimaenaga/scim_users_controller.rb +104 -0
  10. data/app/helpers/{scim_rails → scimaenaga}/application_helper.rb +1 -1
  11. data/app/libraries/scim_patch.rb +2 -2
  12. data/app/libraries/scim_patch_operation.rb +1 -1
  13. data/app/libraries/scim_patch_operation_group.rb +3 -3
  14. data/app/libraries/scim_patch_operation_user.rb +2 -2
  15. data/app/models/{scim_rails → scimaenaga}/application_record.rb +1 -1
  16. data/app/models/scimaenaga/authorize_api_request.rb +39 -0
  17. data/app/models/{scim_rails → scimaenaga}/scim_count.rb +8 -4
  18. data/app/models/scimaenaga/scim_query_parser.rb +49 -0
  19. data/config/routes.rb +1 -1
  20. data/lib/generators/scimaenaga/USAGE +8 -0
  21. data/lib/generators/scimaenaga/scimaenaga_generator.rb +7 -0
  22. data/lib/generators/{scim_rails → scimaenaga}/templates/initializer.rb +22 -22
  23. data/lib/{scim_rails → scimaenaga}/config.rb +2 -2
  24. data/lib/scimaenaga/encoder.rb +27 -0
  25. data/lib/scimaenaga/engine.rb +12 -0
  26. data/lib/scimaenaga/version.rb +5 -0
  27. data/lib/scimaenaga.rb +6 -0
  28. data/lib/tasks/{scim_rails_tasks.rake → scimaenaga_tasks.rake} +1 -1
  29. data/spec/controllers/{scim_rails → scimaenaga}/scim_groups_controller_spec.rb +8 -8
  30. data/spec/controllers/{scim_rails → scimaenaga}/scim_groups_request_spec.rb +18 -18
  31. data/spec/controllers/{scim_rails → scimaenaga}/scim_schemas_controller_spec.rb +7 -7
  32. data/spec/controllers/{scim_rails → scimaenaga}/scim_schemas_request_spec.rb +1 -1
  33. data/spec/controllers/{scim_rails → scimaenaga}/scim_users_controller_spec.rb +14 -15
  34. data/spec/controllers/{scim_rails → scimaenaga}/scim_users_request_spec.rb +20 -20
  35. data/spec/dummy/app/assets/config/manifest.js +1 -1
  36. data/spec/dummy/config/application.rb +1 -2
  37. data/spec/dummy/config/initializers/{scim_rails_config.rb → scimaenaga_config.rb} +1 -1
  38. data/spec/dummy/config/routes.rb +1 -1
  39. data/spec/factories/company.rb +3 -3
  40. data/spec/lib/scimaenaga/encoder_spec.rb +64 -0
  41. data/spec/libraries/scim_patch_operation_group_spec.rb +14 -14
  42. data/spec/libraries/scim_patch_operation_user_spec.rb +5 -5
  43. data/spec/libraries/scim_patch_spec.rb +2 -2
  44. data/spec/models/scim_query_parser_spec.rb +5 -6
  45. metadata +40 -39
  46. data/app/controllers/concerns/scim_rails/response.rb +0 -94
  47. data/app/controllers/scim_rails/application_controller.rb +0 -72
  48. data/app/controllers/scim_rails/scim_users_controller.rb +0 -104
  49. data/app/models/scim_rails/authorize_api_request.rb +0 -40
  50. data/app/models/scim_rails/scim_query_parser.rb +0 -49
  51. data/lib/generators/scim_rails/USAGE +0 -8
  52. data/lib/generators/scim_rails/scim_rails_generator.rb +0 -7
  53. data/lib/scim_rails/encoder.rb +0 -25
  54. data/lib/scim_rails/engine.rb +0 -12
  55. data/lib/scim_rails/version.rb +0 -5
  56. data/lib/scim_rails.rb +0 -6
  57. data/spec/lib/scim_rails/encoder_spec.rb +0 -62
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScimRails
4
- module Response
5
- CONTENT_TYPE = "application/scim+json"
6
-
7
- def json_response(object, status = :ok)
8
- render \
9
- json: object,
10
- status: status,
11
- content_type: CONTENT_TYPE
12
- end
13
-
14
- def json_scim_response(object:, status: :ok, counts: nil)
15
- case params[:action]
16
- when "index"
17
- render \
18
- json: list_response(object, counts),
19
- status: status,
20
- content_type: CONTENT_TYPE
21
- when "show", "create", "put_update", "patch_update"
22
- render \
23
- json: object_response(object),
24
- status: status,
25
- content_type: CONTENT_TYPE
26
- end
27
- end
28
-
29
- private
30
-
31
- def list_response(object, counts)
32
- object = object
33
- .order(:id)
34
- .offset(counts.offset)
35
- .limit(counts.limit)
36
- {
37
- schemas: [
38
- "urn:ietf:params:scim:api:messages:2.0:ListResponse"
39
- ],
40
- totalResults: counts.total,
41
- startIndex: counts.start_index,
42
- itemsPerPage: counts.limit,
43
- Resources: list_objects(object)
44
- }
45
- end
46
-
47
- def list_objects(objects)
48
- objects.map do |object|
49
- object_response(object)
50
- end
51
- end
52
-
53
- def object_response(object)
54
- schema = case object
55
- when ScimRails.config.scim_users_model
56
- ScimRails.config.user_schema
57
- when ScimRails.config.scim_groups_model
58
- ScimRails.config.group_schema
59
- else
60
- raise ScimRails::ExceptionHandler::InvalidQuery,
61
- "Unknown model: #{object}"
62
- end
63
- find_value(object, schema)
64
- end
65
-
66
- # `find_value` is a recursive method that takes a "user" and a
67
- # "user schema" and replaces any symbols in the schema with the
68
- # corresponding value from the user. Given a schema with symbols,
69
- # `find_value` will search through the object for the symbols,
70
- # send those symbols to the model, and replace the symbol with
71
- # the return value.
72
-
73
- def find_value(object, schema)
74
- case schema
75
- when Hash
76
- schema.each.with_object({}) do |(key, value), hash|
77
- hash[key] = find_value(object, value)
78
- end
79
- when Array, ActiveRecord::Associations::CollectionProxy
80
- schema.map do |value|
81
- find_value(object, value)
82
- end
83
- when ScimRails.config.scim_users_model
84
- find_value(schema, ScimRails.config.user_abbreviated_schema)
85
- when ScimRails.config.scim_groups_model
86
- find_value(schema, ScimRails.config.group_abbreviated_schema)
87
- when Symbol
88
- find_value(object, object.public_send(schema))
89
- else
90
- schema
91
- end
92
- end
93
- end
94
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScimRails
4
- class ApplicationController < ActionController::API
5
- include ActionController::HttpAuthentication::Basic::ControllerMethods
6
- include ExceptionHandler
7
- include Response
8
-
9
- before_action :authorize_request
10
-
11
- private
12
-
13
- def authorize_request
14
- send(authentication_strategy) do |searchable_attribute, authentication_attribute|
15
- authorization = AuthorizeApiRequest.new(
16
- searchable_attribute: searchable_attribute,
17
- authentication_attribute: authentication_attribute
18
- )
19
- @company = authorization.company
20
- end
21
- raise ScimRails::ExceptionHandler::InvalidCredentials if @company.blank?
22
- end
23
-
24
- def authentication_strategy
25
- if request.headers["Authorization"]&.include?("Bearer")
26
- :authenticate_with_oauth_bearer
27
- else
28
- :authenticate_with_http_basic
29
- end
30
- end
31
-
32
- def authenticate_with_oauth_bearer
33
- authentication_attribute = request.headers["Authorization"].split.last
34
- payload = ScimRails::Encoder.decode(authentication_attribute).with_indifferent_access
35
- searchable_attribute = payload[ScimRails.config.basic_auth_model_searchable_attribute]
36
-
37
- yield searchable_attribute, authentication_attribute
38
- end
39
-
40
- def find_value_for(attribute)
41
- params.dig(*path_for(attribute))
42
- end
43
-
44
- # `path_for` is a recursive method used to find the "path" for
45
- # `.dig` to take when looking for a given attribute in the
46
- # params.
47
- #
48
- # Example: `path_for(:name)` should return an array that looks
49
- # like [:names, 0, :givenName]. `.dig` can then use that path
50
- # against the params to translate the :name attribute to "John".
51
-
52
- def path_for(attribute, object = controller_schema, path = [])
53
- at_path = path.empty? ? object : object.dig(*path)
54
- return path if at_path == attribute
55
-
56
- case at_path
57
- when Hash
58
- at_path.each do |key, _value|
59
- found_path = path_for(attribute, object, [*path, key])
60
- return found_path if found_path
61
- end
62
- nil
63
- when Array
64
- at_path.each_with_index do |_value, index|
65
- found_path = path_for(attribute, object, [*path, index])
66
- return found_path if found_path
67
- end
68
- nil
69
- end
70
- end
71
- end
72
- end
@@ -1,104 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScimRails
4
- class ScimUsersController < ScimRails::ApplicationController
5
-
6
- def index
7
- if params[:filter].present?
8
- query = ScimRails::ScimQueryParser.new(
9
- params[:filter], ScimRails.config.queryable_user_attributes
10
- )
11
-
12
- users = @company
13
- .public_send(ScimRails.config.scim_users_scope)
14
- .where(
15
- "#{ScimRails.config.scim_users_model
16
- .connection.quote_column_name(query.attribute)} #{query.operator} ?",
17
- query.parameter
18
- )
19
- .order(ScimRails.config.scim_users_list_order)
20
- else
21
- users = @company
22
- .public_send(ScimRails.config.scim_users_scope)
23
- .order(ScimRails.config.scim_users_list_order)
24
- end
25
-
26
- counts = ScimCount.new(
27
- start_index: params[:startIndex],
28
- limit: params[:count],
29
- total: users.count
30
- )
31
-
32
- json_scim_response(object: users, counts: counts)
33
- end
34
-
35
- def create
36
- if ScimRails.config.scim_user_prevent_update_on_create
37
- user = @company
38
- .public_send(ScimRails.config.scim_users_scope)
39
- .create!(permitted_user_params)
40
- else
41
- username_key = ScimRails.config.queryable_user_attributes[:userName]
42
- find_by_username = {}
43
- find_by_username[username_key] = permitted_user_params[username_key]
44
- user = @company
45
- .public_send(ScimRails.config.scim_users_scope)
46
- .find_or_create_by(find_by_username)
47
- user.update!(permitted_user_params)
48
- end
49
- json_scim_response(object: user, status: :created)
50
- end
51
-
52
- def show
53
- user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
54
- json_scim_response(object: user)
55
- end
56
-
57
- def put_update
58
- user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
59
- user.update!(permitted_user_params)
60
- json_scim_response(object: user)
61
- end
62
-
63
- def patch_update
64
- user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
65
- patch = ScimPatch.new(params, :user)
66
- patch.save(user)
67
-
68
- json_scim_response(object: user)
69
- end
70
-
71
- def destroy
72
- unless ScimRails.config.user_destroy_method
73
- raise ScimRails::ExceptionHandler::InvalidConfiguration
74
- end
75
-
76
- user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
77
- raise ActiveRecord::RecordNotFound unless user
78
-
79
- begin
80
- user.public_send(ScimRails.config.user_destroy_method)
81
- rescue NoMethodError => e
82
- raise ScimRails::ExceptionHandler::InvalidConfiguration, e.message
83
- rescue ActiveRecord::RecordNotDestroyed => e
84
- raise ScimRails::ExceptionHandler::InvalidRequest, e.message
85
- rescue => e
86
- raise ScimRails::ExceptionHandler::UnexpectedError, e.message
87
- end
88
-
89
- head :no_content
90
- end
91
-
92
- private
93
-
94
- def permitted_user_params
95
- ScimRails.config.mutable_user_attributes.each.with_object({}) do |attribute, hash|
96
- hash[attribute] = find_value_for(attribute)
97
- end
98
- end
99
-
100
- def controller_schema
101
- ScimRails.config.mutable_user_attributes_schema
102
- end
103
- end
104
- end
@@ -1,40 +0,0 @@
1
- module ScimRails
2
- class AuthorizeApiRequest
3
-
4
- def initialize(searchable_attribute:, authentication_attribute:)
5
- @searchable_attribute = searchable_attribute
6
- @authentication_attribute = authentication_attribute
7
-
8
- raise ScimRails::ExceptionHandler::InvalidCredentials if searchable_attribute.blank? || authentication_attribute.blank?
9
-
10
- @search_parameter = { ScimRails.config.basic_auth_model_searchable_attribute => @searchable_attribute }
11
- end
12
-
13
- def company
14
- company = find_company
15
- authorize(company)
16
- company
17
- end
18
-
19
- private
20
-
21
- attr_reader :authentication_attribute
22
- attr_reader :search_parameter
23
- attr_reader :searchable_attribute
24
-
25
- def find_company
26
- @company ||= ScimRails.config.basic_auth_model.find_by!(search_parameter)
27
-
28
- rescue ActiveRecord::RecordNotFound
29
- raise ScimRails::ExceptionHandler::InvalidCredentials
30
- end
31
-
32
- def authorize(authentication_model)
33
- authorized = ActiveSupport::SecurityUtils.secure_compare(
34
- authentication_model.public_send(ScimRails.config.basic_auth_model_authenticatable_attribute),
35
- authentication_attribute
36
- )
37
- raise ScimRails::ExceptionHandler::InvalidCredentials unless authorized
38
- end
39
- end
40
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScimRails
4
- class ScimQueryParser
5
- attr_accessor :query_elements, :query_attributes
6
-
7
- def initialize(query_string, queryable_attributes)
8
- self.query_elements = query_string.gsub(/\[(.+?)\]/, ".0").split
9
- self.query_attributes = queryable_attributes
10
- end
11
-
12
- def attribute
13
- attribute = query_elements[0]
14
- raise ScimRails::ExceptionHandler::InvalidQuery if attribute.blank?
15
-
16
- dig_keys = attribute.split(".").map do |step|
17
- step == "0" ? 0 : step.to_sym
18
- end
19
-
20
- mapped_attribute = query_attributes.dig(*dig_keys)
21
- raise ScimRails::ExceptionHandler::InvalidQuery if mapped_attribute.blank?
22
-
23
- mapped_attribute
24
- end
25
-
26
- def operator
27
- sql_comparison_operator(query_elements[1])
28
- end
29
-
30
- def parameter
31
- parameter = query_elements[2..-1].join(" ")
32
- return if parameter.blank?
33
-
34
- parameter.gsub(/"/, "")
35
- end
36
-
37
- private
38
-
39
- def sql_comparison_operator(element)
40
- case element
41
- when "eq"
42
- "="
43
- else
44
- # TODO: implement additional query filters
45
- raise ScimRails::ExceptionHandler::InvalidQuery
46
- end
47
- end
48
- end
49
- end
@@ -1,8 +0,0 @@
1
- Description:
2
- Generates the scim_rails initializer.
3
-
4
- Example:
5
- rails generate scim_rails config
6
-
7
- This will create:
8
- config/initializers/scim_rails_config.rb
@@ -1,7 +0,0 @@
1
- class ScimRailsGenerator < Rails::Generators::NamedBase
2
- source_root File.expand_path('../templates', __FILE__)
3
-
4
- def copy_initializer_file
5
- copy_file "initializer.rb", "config/initializers/scim_rails_config.rb"
6
- end
7
- end
@@ -1,25 +0,0 @@
1
- require "jwt"
2
-
3
- module ScimRails
4
- module Encoder
5
- extend self
6
-
7
- def encode(company)
8
- payload = {
9
- iat: Time.current.to_i,
10
- ScimRails.config.basic_auth_model_searchable_attribute =>
11
- company.public_send(ScimRails.config.basic_auth_model_searchable_attribute)
12
- }
13
-
14
- JWT.encode(payload, ScimRails.config.signing_secret, ScimRails.config.signing_algorithm)
15
- end
16
-
17
- def decode(token)
18
- verify = ScimRails.config.signing_algorithm != ScimRails::Config::ALGO_NONE
19
-
20
- JWT.decode(token, ScimRails.config.signing_secret, verify, algorithm: ScimRails.config.signing_algorithm).first
21
- rescue JWT::VerificationError, JWT::DecodeError
22
- raise ScimRails::ExceptionHandler::InvalidCredentials
23
- end
24
- end
25
- end
@@ -1,12 +0,0 @@
1
- module ScimRails
2
- class Engine < ::Rails::Engine
3
- isolate_namespace ScimRails
4
-
5
- config.generators do |g|
6
- g.test_framework :rspec, :fixture => false
7
- g.fixture_replacement :factory_bot, :dir => "spec/factories"
8
- g.assets false
9
- g.helper false
10
- end
11
- end
12
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ScimRails
4
- VERSION = '0.9.0'
5
- end
data/lib/scim_rails.rb DELETED
@@ -1,6 +0,0 @@
1
- require "scim_rails/engine"
2
- require "scim_rails/config"
3
- require "scim_rails/encoder"
4
-
5
- module ScimRails
6
- end
@@ -1,62 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe ScimRails::Encoder do
4
- let(:company) { Company.new(subdomain: "test") }
5
-
6
- describe "::encode" do
7
- context "with signing configuration" do
8
- it "generates a signed token with the company attribute" do
9
- token = ScimRails::Encoder.encode(company)
10
- payload = ScimRails::Encoder.decode(token)
11
-
12
- expect(token).to match /[a-z|A-Z|0-9.]{16,}\.[a-z|A-Z|0-9.]{16,}/
13
- expect(payload).to contain_exactly(["iat", Integer], ["subdomain", "test"])
14
- end
15
- end
16
-
17
- context "without signing configuration" do
18
- before do
19
- allow(ScimRails.config).to receive(:signing_secret).and_return(nil)
20
- allow(ScimRails.config).to receive(:signing_algorithm).and_return(ScimRails::Config::ALGO_NONE)
21
- end
22
-
23
- it "generates an unsigned token with the company attribute" do
24
- token = ScimRails::Encoder.encode(company)
25
- payload = ScimRails::Encoder.decode(token)
26
-
27
- expect(token).to match /[a-z|A-Z|0-9.]{16,}/
28
- expect(payload).to contain_exactly(["iat", Integer], ["subdomain", "test"])
29
- end
30
- end
31
- end
32
-
33
- describe "::decode" do
34
- let(:token) { ScimRails::Encoder.encode(company) }
35
-
36
- it "raises InvalidCredentials error for an invalid token" do
37
- token = "f487bf84bfub4f74fj4894fnh483f4h4u8f"
38
- expect { ScimRails::Encoder.decode(token) }.to raise_error ScimRails::ExceptionHandler::InvalidCredentials
39
- end
40
-
41
- context "with signing configuration" do
42
- it "decodes a signed token, returning the company attributes" do
43
- payload = ScimRails::Encoder.decode(token)
44
-
45
- expect(payload).to contain_exactly(["iat", Integer], ["subdomain", "test"])
46
- end
47
- end
48
-
49
- context "without signing configuration" do
50
- before do
51
- allow(ScimRails.config).to receive(:signing_secret).and_return(nil)
52
- allow(ScimRails.config).to receive(:signing_algorithm).and_return(ScimRails::Config::ALGO_NONE)
53
- end
54
-
55
- it "decodes an unsigned token, returning the company attributes" do
56
- payload = ScimRails::Encoder.decode(token)
57
-
58
- expect(payload).to contain_exactly(["iat", Integer], ["subdomain", "test"])
59
- end
60
- end
61
- end
62
- end