jsonapi_parameters 2.1.1 → 2.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5de39e34dba3594c8bf020543481daa14945aff005cc537068b6a6eb49c9c219
4
- data.tar.gz: 941dea9a1145a4efd538795eb3280a5000c767b00b86be97353a320f5074042d
3
+ metadata.gz: 8bcf0cd62169a18d8f485540f7fe27e85d2ed8278121fe19309d149b5c59141a
4
+ data.tar.gz: eef89bf8b7061bf21add79bd09ce3b164c6b0d91e0d8db2898de99e231ab7d36
5
5
  SHA512:
6
- metadata.gz: 8a0e1b627dc18c7aa8ccfcdee552a0cdef0ef767661f638e612ea03f020ae265a0402409bf440d5e9d2ce48448926f4e1cfd416d69760521203dd5c16de23cc4
7
- data.tar.gz: fcc264b264836ce8925979fa053965a9b6ccdcc0b813040fe30b0ece91653f1390b94a7d546ab16060824043b33eb257a2ed9c3214e7fbe9fd0076319ab8a320
6
+ metadata.gz: fbb1868c672869c4e0b561012dba89ed444271bef9246be22df68b564e397e21d1058860ce5416748c5a662f960bbe5c424db56882e2163bc8ae6516861de00e
7
+ data.tar.gz: '0940555db68b3b48522dbb33d6f035334ff8b149731b71b597df5e148b0d0d900141dd5213202c76eac4a406b2721c273773a401d90ae94caf44ae90b7a0c34c'
data/README.md CHANGED
@@ -93,7 +93,53 @@ translator.jsonapify(params)
93
93
 
94
94
  As [stated in the JSON:API specification](https://jsonapi.org/#mime-types) correct mime type for JSON:API input should be [`application/vnd.api+json`](http://www.iana.org/assignments/media-types/application/vnd.api+json).
95
95
 
96
- This gems intention is to make input consumption as easy as possible. Hence, it [registers this mime type for you](lib/jsonapi_parameters/core_ext/action_dispatch/http/mime_type.rb).
96
+ This gem's intention is to make input consumption as easy as possible. Hence, it [registers this mime type for you](lib/jsonapi_parameters/core_ext/action_dispatch/http/mime_type.rb).
97
+
98
+ ## Stack limit
99
+
100
+ In theory, any payload may consist of infinite amount of relationships (and so each relationship may have its own, included, infinite amount of nested relationships).
101
+ Because of that, it is a potential vector of attack.
102
+
103
+ For this reason we have introduced a default limit of stack levels that JsonApi::Parameters will go down through while parsing the payloads.
104
+
105
+ This default limit is 3, and can be overwritten by specifying the custom limit.
106
+
107
+ #### Ruby
108
+ ```
109
+ class Translator
110
+ include JsonApi::Parameters
111
+ end
112
+
113
+ translator = Translator.new
114
+
115
+ translator.jsonapify(custom_stack_limit: 4)
116
+
117
+ # OR
118
+
119
+ translator.stack_limit = 4
120
+ translator.jsonapify.(...)
121
+ ```
122
+
123
+ #### Rails
124
+ ```
125
+ # config/initializers/jsonapi_parameters.rb
126
+
127
+ def create_params
128
+ params.from_jsonapi(custom_stack_limit: 4).require(:user).permit(
129
+ entities_attributes: { subentities_attributes: { ... } }
130
+ )
131
+ end
132
+
133
+ # OR
134
+
135
+ def create_params
136
+ params.stack_level = 4
137
+
138
+ params.from_jsonapi.require(:user).permit(entities_attributes: { subentities_attributes: { ... } })
139
+ ensure
140
+ params.reset_stack_limit!
141
+ end
142
+ ```
97
143
 
98
144
  ## Customization
99
145
 
@@ -2,6 +2,5 @@ require 'jsonapi_parameters/parameters'
2
2
  require 'jsonapi_parameters/handlers'
3
3
  require 'jsonapi_parameters/translator'
4
4
  require 'jsonapi_parameters/core_ext'
5
+ require 'jsonapi_parameters/stack_limit'
5
6
  require 'jsonapi_parameters/version'
6
-
7
- require 'active_support/inflector'
@@ -9,7 +9,7 @@ class ActionController::Parameters
9
9
  from_jsonapi(*args)
10
10
  end
11
11
 
12
- def from_jsonapi(naming_convention = :snake)
13
- @from_jsonapi ||= self.class.new jsonapify(self, naming_convention: naming_convention)
12
+ def from_jsonapi(naming_convention = :snake, custom_stack_limit: stack_limit)
13
+ @from_jsonapi ||= self.class.new jsonapify(self, naming_convention: naming_convention, custom_stack_limit: custom_stack_limit)
14
14
  end
15
15
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/inflector'
2
+
1
3
  module JsonApi
2
4
  module Parameters
3
5
  module Handlers
@@ -34,8 +36,9 @@ module JsonApi
34
36
  @with_inclusion &= !included_object.empty?
35
37
 
36
38
  if with_inclusion
37
- included_object.delete(:type)
38
- included_object[:attributes].merge(id: related_id)
39
+ { **(included_object[:attributes] || {}), id: related_id }.tap do |body|
40
+ body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships
41
+ end
39
42
  else
40
43
  relationship.dig(:id)
41
44
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/inflector'
2
+
1
3
  module JsonApi
2
4
  module Parameters
3
5
  module Handlers
@@ -15,8 +17,10 @@ module JsonApi
15
17
 
16
18
  return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty?
17
19
 
18
- included_object.delete(:type)
19
- included_object = included_object[:attributes].merge(id: related_id)
20
+ included_object = { **(included_object[:attributes] || {}), id: related_id }.tap do |body|
21
+ body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships
22
+ end
23
+
20
24
  ["#{singularize(relationship_key)}_attributes".to_sym, included_object]
21
25
  end
22
26
  end
@@ -29,7 +29,7 @@ module JsonApi
29
29
  resource_handlers[resource_key.to_sym] = handler_key.to_sym
30
30
  end
31
31
 
32
- def reset_handlers!
32
+ def reset_handlers
33
33
  @handlers = DEFAULT_HANDLER_SET.dup
34
34
  @resource_handlers = {}
35
35
  end
@@ -0,0 +1,45 @@
1
+ module JsonApi
2
+ module Parameters
3
+ LIMIT = 3
4
+
5
+ class StackLevelTooDeepError < StandardError
6
+ end
7
+
8
+ def stack_limit=(val)
9
+ @stack_limit = val
10
+ end
11
+
12
+ def stack_limit
13
+ @stack_limit || LIMIT
14
+ end
15
+
16
+ def reset_stack_limit
17
+ @stack_limit = LIMIT
18
+ end
19
+
20
+ private
21
+
22
+ def initialize_stack(custom_stack_limit)
23
+ @current_stack_level = 0
24
+ @stack_limit = custom_stack_limit
25
+ end
26
+
27
+ def increment_stack_level!
28
+ @current_stack_level += 1
29
+
30
+ raise StackLevelTooDeepError.new(stack_exception_message) if @current_stack_level > stack_limit
31
+ end
32
+
33
+ def decrement_stack_level
34
+ @current_stack_level -= 1
35
+ end
36
+
37
+ def reset_stack_level
38
+ @current_stack_level = 0
39
+ end
40
+
41
+ def stack_exception_message
42
+ "Stack level of nested payload is too deep: #{@current_stack_level}/#{stack_limit}. Please see the documentation on how to overwrite the limit."
43
+ end
44
+ end
45
+ end
@@ -3,7 +3,9 @@ require 'active_support/inflector'
3
3
  module JsonApi::Parameters
4
4
  include ActiveSupport::Inflector
5
5
 
6
- def jsonapify(params, naming_convention: :snake)
6
+ def jsonapify(params, naming_convention: :snake, custom_stack_limit: stack_limit)
7
+ initialize_stack(custom_stack_limit)
8
+
7
9
  jsonapi_translate(params, naming_convention: naming_convention)
8
10
  end
9
11
 
@@ -38,27 +40,11 @@ module JsonApi::Parameters
38
40
  def jsonapi_main_body
39
41
  jsonapi_unsafe_params.tap do |param|
40
42
  jsonapi_relationships.each do |relationship_key, relationship_value|
41
- relationship_value = relationship_value[:data]
42
- handler_args = [relationship_key, relationship_value, jsonapi_included]
43
- handler = if Handlers.resource_handlers.key?(relationship_key)
44
- Handlers.handlers[Handlers.resource_handlers[relationship_key]]
45
- else
46
- case relationship_value
47
- when Array
48
- Handlers.handlers[:to_many]
49
- when Hash
50
- Handlers.handlers[:to_one]
51
- when nil
52
- Handlers.handlers[:nil]
53
- else
54
- raise NotImplementedError.new('relationship resource linkage has to be a type of Array, Hash or nil')
55
- end
56
- end
57
-
58
- key, val = handler.call(*handler_args)
59
- param[key] = val
43
+ param = handle_relationships(param, relationship_key, relationship_value)
60
44
  end
61
45
  end
46
+ ensure
47
+ reset_stack_level
62
48
  end
63
49
 
64
50
  def jsonapi_unsafe_params
@@ -69,11 +55,63 @@ module JsonApi::Parameters
69
55
  end
70
56
  end
71
57
 
58
+ def jsonapi_relationships
59
+ @jsonapi_relationships ||= @jsonapi_unsafe_hash.dig(:data, :relationships) || []
60
+ end
61
+
72
62
  def jsonapi_included
73
63
  @jsonapi_included ||= @jsonapi_unsafe_hash[:included] || []
74
64
  end
75
65
 
76
- def jsonapi_relationships
77
- @jsonapi_relationships ||= @jsonapi_unsafe_hash.dig(:data, :relationships) || []
66
+ def handle_relationships(param, relationship_key, relationship_value)
67
+ increment_stack_level!
68
+
69
+ relationship_value = relationship_value[:data]
70
+ handler_args = [relationship_key, relationship_value, jsonapi_included]
71
+ handler = if Handlers.resource_handlers.key?(relationship_key)
72
+ Handlers.handlers[Handlers.resource_handlers[relationship_key]]
73
+ else
74
+ case relationship_value
75
+ when Array
76
+ Handlers.handlers[:to_many]
77
+ when Hash
78
+ Handlers.handlers[:to_one]
79
+ when nil
80
+ Handlers.handlers[:nil]
81
+ else
82
+ raise NotImplementedError.new('relationship resource linkage has to be a type of Array, Hash or nil')
83
+ end
84
+ end
85
+
86
+ key, val = handler.call(*handler_args)
87
+
88
+ param[key] = handle_nested_relationships(val)
89
+
90
+ param
91
+ ensure
92
+ decrement_stack_level
93
+ end
94
+
95
+ def handle_nested_relationships(val)
96
+ # We can only consider Hash relationships (which imply to-one relationship) and Array relationships (which imply to-many).
97
+ # Each type has a different handling method, though in both cases we end up passing the nested relationship recursively to handle_relationship
98
+ # (and yes, this may go on indefinitely, basically we're going by the relationship levels, deeper and deeper)
99
+ case val
100
+ when Array
101
+ relationships_with_nested_relationships = val.select { |rel| rel.is_a?(Hash) && rel.dig(:relationships) }
102
+ relationships_with_nested_relationships.each do |relationship_with_nested_relationship|
103
+ relationship_with_nested_relationship.delete(:relationships).each do |rel_key, rel_val|
104
+ relationship_with_nested_relationship = handle_relationships(relationship_with_nested_relationship, rel_key, rel_val)
105
+ end
106
+ end
107
+ when Hash
108
+ if val.key?(:relationships)
109
+ val.delete(:relationships).each do |rel_key, rel_val|
110
+ val = handle_relationships(val, rel_key, rel_val)
111
+ end
112
+ end
113
+ end
114
+
115
+ val
78
116
  end
79
117
  end
@@ -1,5 +1,5 @@
1
1
  module JsonApi
2
2
  module Parameters
3
- VERSION = '2.1.1'.freeze
3
+ VERSION = '2.2.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi_parameters
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Visuality
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-07-31 00:00:00.000000000 Z
12
+ date: 2020-08-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -327,6 +327,7 @@ files:
327
327
  - lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb
328
328
  - lib/jsonapi_parameters/handlers.rb
329
329
  - lib/jsonapi_parameters/parameters.rb
330
+ - lib/jsonapi_parameters/stack_limit.rb
330
331
  - lib/jsonapi_parameters/translator.rb
331
332
  - lib/jsonapi_parameters/version.rb
332
333
  homepage: https://github.com/visualitypl/jsonapi_parameters