gestart-admino 2.1.0 → 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: 576292443287e49e641f58374a71998a7b22f495bf55aeb4e002d6ad1afeba04
4
- data.tar.gz: 3fa6cc8fe5fa209113b26f656600eaa49072fb6ea66946daa4f900881ee00691
3
+ metadata.gz: 6eee888637add8e2859414f6d4c9289fefa200ca94bcde3059136c6a348a41fa
4
+ data.tar.gz: 35d119dca454b08b6e480877332dc0dc437522713022ec4cdcf4e08a56c6acf9
5
5
  SHA512:
6
- metadata.gz: 161818d2fc869089fa7c6fb9ea4ffe16f9aa51a91a364484fdce686541dfdad3b8b41a7b9362a3615c9d1a3145324f4ba40fe66676f176dbb3523a3489cc1a8e
7
- data.tar.gz: 32d718e86ffee8943eef0f6e55a8a709ae8ec5908c026f1ff4e1d76e8acf815f61864cb7548d514c6d1976fc2115d46cc9c3bd36ee558332b7b05a74b9fb99db
6
+ metadata.gz: aa3e6560efc97e87fb8b06799d9b56bd641b0d21c70052eac8172e8637629606fe119f415b74ee336377e2f8740aaa530d2006029daa5c5cbfef0d8b8314bbe3
7
+ data.tar.gz: bd04372a12ec42e70087ededfa4a6f3e510dd259d970ea7af4fe4d778b79d6aa6367b1531579a9ac09076012ab37b638327ddb4ebbc4b0df7b4a6e54ad2a51b7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ # v2.2.0 — parameters are filtered against the configuration
2
+
3
+ `Query::Base#initialize` now drops anything the query object has not declared,
4
+ instead of handing the whole params hash to the scopes. This is the SEC-4
5
+ finding of Gestart's 27/07 audit, and it closes in one place for every caller —
6
+ controllers, interactors and background jobs alike — because every chain goes
7
+ through this constructor.
8
+
9
+ The whitelist is not new configuration: it is read from what the query object
10
+ already declares. `search_field` and `filter_by` names are permitted inside the
11
+ nested `query` hash; `page`, `sorting` and `sort_order` are permitted at the top
12
+ level. Nothing else survives.
13
+
14
+ `permit_params` is the hook for a query object that legitimately reads another
15
+ top-level key:
16
+
17
+ ```ruby
18
+ class ThingQuery < GestartAdmino::Query::Base
19
+ permit_params :scope_id
20
+ end
21
+ ```
22
+
23
+ Filtering happens after the params are normalised, so it applies however they
24
+ arrived — `to_unsafe_h`, a plain hash, or a hash rebuilt by a job.
25
+
26
+ Note that this does not make interpolated `order` clauses safe, nor does it
27
+ validate the values themselves: it bounds which keys exist, not what they hold.
28
+
29
+ ---
30
+
1
31
  # v2.1.0 — off showcase, onto gestart-showcase
2
32
 
3
33
  The `showcase` dependency becomes `gestart-showcase ~> 1.0`, and the eight files
@@ -17,6 +17,8 @@ module GestartAdmino
17
17
  include ActiveModel::Conversion
18
18
  extend Dsl
19
19
 
20
+ TOP_LEVEL_PARAMS = %i[page sorting sort_order].freeze
21
+
20
22
  attr_reader :params
21
23
  attr_reader :context
22
24
  attr_reader :filter_groups
@@ -28,14 +30,16 @@ module GestartAdmino
28
30
  end
29
31
 
30
32
  def initialize(params = nil, context = {}, config = nil)
31
- @params =
33
+ @config = config
34
+ @context = context
35
+
36
+ raw =
32
37
  if params.respond_to?(:to_unsafe_h)
33
38
  ActiveSupport::HashWithIndifferentAccess.new(params.to_unsafe_h)
34
39
  else
35
40
  ActiveSupport::HashWithIndifferentAccess.new(params)
36
41
  end
37
- @config = config
38
- @context = context
42
+ @params = permitted_params(raw)
39
43
 
40
44
  init_filter_groups
41
45
  init_search_fields
@@ -119,6 +123,17 @@ module GestartAdmino
119
123
  @sorting = Sorting.new(config.sorting, params, i18n_key)
120
124
  end
121
125
  end
126
+
127
+ def permitted_params(raw)
128
+ filtered = raw.slice(*TOP_LEVEL_PARAMS, *config.permitted_params)
129
+
130
+ nested = raw[:query]
131
+ if nested.is_a?(Hash)
132
+ filtered[:query] = nested.slice(*config.declared_query_keys)
133
+ end
134
+
135
+ filtered
136
+ end
122
137
  end
123
138
  end
124
139
  end
@@ -71,12 +71,22 @@ module GestartAdmino
71
71
  attr_reader :search_fields
72
72
  attr_reader :filter_groups
73
73
  attr_reader :sorting
74
+ attr_reader :permitted_params
74
75
  attr_accessor :starting_scope_callable
75
76
  attr_accessor :ending_scope_callable
76
77
 
77
78
  def initialize
78
79
  @search_fields = []
79
80
  @filter_groups = []
81
+ @permitted_params = []
82
+ end
83
+
84
+ def add_permitted_params(*names)
85
+ @permitted_params |= names.flatten.map(&:to_sym)
86
+ end
87
+
88
+ def declared_query_keys
89
+ search_fields.map(&:name) + filter_groups.map(&:name)
80
90
  end
81
91
 
82
92
  def add_search_field(name, options = {})
@@ -26,6 +26,10 @@ module GestartAdmino
26
26
  config.add_sorting_scopes(args, options)
27
27
  end
28
28
 
29
+ def permit_params(*names)
30
+ config.add_permitted_params(*names)
31
+ end
32
+
29
33
  def starting_scope(&block)
30
34
  config.starting_scope_callable = block
31
35
  end
@@ -1,3 +1,3 @@
1
1
  module GestartAdmino
2
- VERSION = "2.1.0"
2
+ VERSION = "2.2.0"
3
3
  end
@@ -186,6 +186,103 @@ module GestartAdmino
186
186
  end
187
187
  end
188
188
  end
189
+
190
+ describe 'parameter filtering' do
191
+ let(:config) { Configuration.new }
192
+
193
+ before do
194
+ config.add_search_field(:search_field)
195
+ config.add_filter_group(:filter_group, [:one, :two])
196
+ config.add_sorting_scopes([:title, :year])
197
+ end
198
+
199
+ context 'with an undeclared nested key' do
200
+ let(:params) do
201
+ { query: { search_field: 'foo', injected: 'evil' } }
202
+ end
203
+
204
+ it 'drops it' do
205
+ expect(query.params[:query]).to eq('search_field' => 'foo')
206
+ end
207
+
208
+ it 'keeps it away from the scope' do
209
+ result = query.scope(ScopeMock.new('start'))
210
+ expect(result.chain).not_to include(:injected)
211
+ end
212
+ end
213
+
214
+ context 'with an undeclared top level key' do
215
+ let(:params) { { evil: 'payload', sorting: 'title' } }
216
+
217
+ it 'drops it and keeps the declared one' do
218
+ expect(query.params.keys).to eq %w[sorting]
219
+ end
220
+ end
221
+
222
+ context 'with declared keys' do
223
+ let(:params) do
224
+ {
225
+ query: { search_field: 'foo', filter_group: 'one' },
226
+ sorting: 'title',
227
+ sort_order: 'desc'
228
+ }
229
+ end
230
+
231
+ it 'passes them through untouched' do
232
+ expect(query.params).to eq(
233
+ 'query' => { 'search_field' => 'foo', 'filter_group' => 'one' },
234
+ 'sorting' => 'title',
235
+ 'sort_order' => 'desc'
236
+ )
237
+ end
238
+
239
+ it 'still builds the whole scope chain' do
240
+ result = query.scope(ScopeMock.new('start'))
241
+ expect(result.chain).to eq [
242
+ :search_field, ['foo'],
243
+ :one, [],
244
+ :title, [:desc]
245
+ ]
246
+ end
247
+ end
248
+
249
+ context 'with page' do
250
+ let(:params) { { page: '2' } }
251
+
252
+ it 'keeps it, even though no config declares it' do
253
+ expect(query.params[:page]).to eq '2'
254
+ end
255
+ end
256
+
257
+ context 'with a sorting value outside the declared scopes' do
258
+ let(:params) { { sorting: 'evil_scope' } }
259
+
260
+ it 'keeps the key but leaves the scope unapplied, as before' do
261
+ result = query.scope(ScopeMock.new('start'))
262
+ expect(result.chain).to eq []
263
+ end
264
+ end
265
+
266
+ context 'with a query object declaring extra top level params' do
267
+ let(:query_klass) do
268
+ Class.new(Base) do
269
+ permit_params :pending
270
+
271
+ def self.model_name
272
+ ActiveModel::Name.new(self, nil, 'temp')
273
+ end
274
+ end
275
+ end
276
+
277
+ subject(:query) { query_klass.new(params) }
278
+
279
+ let(:params) { { pending: 'true', evil: 'payload' } }
280
+
281
+ it 'keeps the declared one and drops the rest' do
282
+ expect(query.params.keys).to eq %w[pending]
283
+ end
284
+ end
285
+ end
189
286
  end
190
287
  end
191
288
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gestart-admino
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gestart Cloud S.r.l.