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 +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/gestart_admino/query/base.rb +18 -3
- data/lib/gestart_admino/query/configuration.rb +10 -0
- data/lib/gestart_admino/query/dsl.rb +4 -0
- data/lib/gestart_admino/version.rb +1 -1
- data/spec/gestart_admino/query/base_spec.rb +97 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6eee888637add8e2859414f6d4c9289fefa200ca94bcde3059136c6a348a41fa
|
|
4
|
+
data.tar.gz: 35d119dca454b08b6e480877332dc0dc437522713022ec4cdcf4e08a56c6acf9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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 = {})
|
|
@@ -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
|