jsonapi_parameters 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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