full_metal_body 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +227 -0
- data/Rakefile +3 -0
- data/lib/full_metal_body/controllers/concerns/input_validation_action.rb +180 -0
- data/lib/full_metal_body/deep_sort.rb +43 -0
- data/lib/full_metal_body/dynamic_whitelist_generator.rb +201 -0
- data/lib/full_metal_body/input_key_utils.rb +29 -0
- data/lib/full_metal_body/input_validation.rb +148 -0
- data/lib/full_metal_body/internal/input_file_validator.rb +47 -0
- data/lib/full_metal_body/internal/input_string_validator.rb +60 -0
- data/lib/full_metal_body/internal/reasonable_boolean_validator.rb +16 -0
- data/lib/full_metal_body/internal/reasonable_date_validator.rb +27 -0
- data/lib/full_metal_body/models/blocked_action.rb +12 -0
- data/lib/full_metal_body/models/blocked_key.rb +12 -0
- data/lib/full_metal_body/railtie.rb +4 -0
- data/lib/full_metal_body/services/save_blocked_keys_service.rb +55 -0
- data/lib/full_metal_body/version.rb +3 -0
- data/lib/full_metal_body/whitelist_writer.rb +64 -0
- data/lib/full_metal_body.rb +9 -0
- data/lib/generators/full_metal_body/install/install_generator.rb +37 -0
- data/lib/generators/full_metal_body/install/templates/create_blocked_actions.rb.erb +22 -0
- data/lib/tasks/full_metal_body_tasks.rake +4 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5913971dd94fbbab8523c89193d31ee59f62ea723ebdfcafd0b93d935c861a1a
|
4
|
+
data.tar.gz: f6ad58265ae64f3800256909269a7486b2fa3f37496e48cab82700a71c6e6e00
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4f12a070d08fe7fa391893103a6089ab60b64eb3b123c3e4dc6a1cb78d4dcef64aa705720f7e9ecc1deb17c26c33d395694d4128764a27f51d6f6ba29c6b577d
|
7
|
+
data.tar.gz: 8353c73e9630609a6b4c7a5200ed9133044c264b7e912ccd0c33014dbf36d11de00761c9fc2ec9672a65cee903af623b3a6559ba95ce4188cc0a075555b65f77
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 patorash
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# FullMetalBody
|
2
|
+
|
3
|
+
[](https://github.com/hayashima/full_metal_body/actions/workflows/test.yml)
|
4
|
+
|
5
|
+
FullMetalBody is a Rails Plugin for input validation in the before_action stage.
|
6
|
+
|
7
|
+
If you write a whitelist of parameters in a YAML file, only allowed keys and values will be passed through.
|
8
|
+
|
9
|
+
However, in other cases, for example, if it detects a string with control characters or a large number of strings aiming for overflow, it will immediately return `400 Bad Request`.
|
10
|
+
|
11
|
+
### Format of whitelist
|
12
|
+
|
13
|
+
```yaml
|
14
|
+
---
|
15
|
+
controller_name:
|
16
|
+
action_name:
|
17
|
+
parameter_name:
|
18
|
+
type: (string|number|date|boolean)
|
19
|
+
options:
|
20
|
+
validator_option1: (value)
|
21
|
+
validator_option2: (value)
|
22
|
+
array_parameters:
|
23
|
+
type: array
|
24
|
+
properties:
|
25
|
+
parameter_name:
|
26
|
+
type: (string|number|date|boolean)
|
27
|
+
options:
|
28
|
+
validator_option1: (value)
|
29
|
+
validator_option2: (value)
|
30
|
+
```
|
31
|
+
|
32
|
+
### Sample of whitelist
|
33
|
+
|
34
|
+
For example, suppose you have created a Scaffold for the `Article` model as shown below.
|
35
|
+
|
36
|
+
```bash
|
37
|
+
bin/rails g scaffold Article title:string content:text
|
38
|
+
```
|
39
|
+
|
40
|
+
The whitelist for it is as follows.
|
41
|
+
|
42
|
+
```yaml
|
43
|
+
---
|
44
|
+
articles:
|
45
|
+
index:
|
46
|
+
p:
|
47
|
+
type: number
|
48
|
+
show:
|
49
|
+
id:
|
50
|
+
type: number
|
51
|
+
create:
|
52
|
+
article:
|
53
|
+
title:
|
54
|
+
type: string
|
55
|
+
content:
|
56
|
+
type: string
|
57
|
+
options:
|
58
|
+
max_length: 4096
|
59
|
+
edit:
|
60
|
+
id:
|
61
|
+
type: number
|
62
|
+
update:
|
63
|
+
id:
|
64
|
+
type: number
|
65
|
+
article:
|
66
|
+
title:
|
67
|
+
type: string
|
68
|
+
content:
|
69
|
+
type: string
|
70
|
+
options:
|
71
|
+
max_length: 4096
|
72
|
+
destroy:
|
73
|
+
id:
|
74
|
+
type: number
|
75
|
+
```
|
76
|
+
|
77
|
+
## Table of contents
|
78
|
+
|
79
|
+
* [FullMetalBody](#fullmetalbody)
|
80
|
+
* [Table of contents](#tableofcontents)
|
81
|
+
* [Installation](#installation)
|
82
|
+
* [Usage](#usage)
|
83
|
+
* [Migration](#migration)
|
84
|
+
* [Modify ApplicationController](#modifyapplicationcontroller)
|
85
|
+
* [Creating a whitelist template](#creatingawhitelisttemplate)
|
86
|
+
* [If you want to allow all parameters](#ifyouwanttoallowallparameters)
|
87
|
+
* [Development](#development)
|
88
|
+
* [Preparation](#preparation)
|
89
|
+
* [Test](#test)
|
90
|
+
* [Contributing](#contributing)
|
91
|
+
* [License](#license)
|
92
|
+
|
93
|
+
## Installation
|
94
|
+
|
95
|
+
Add this line to your application's Gemfile:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
gem "full_metal_body"
|
99
|
+
```
|
100
|
+
|
101
|
+
And then execute:
|
102
|
+
|
103
|
+
```bash
|
104
|
+
$ bundle
|
105
|
+
```
|
106
|
+
|
107
|
+
## Usage
|
108
|
+
|
109
|
+
### Migration
|
110
|
+
|
111
|
+
Create a migration file to store controllers, actions, and parameter keys that do not exist in the whitelist in the database.
|
112
|
+
|
113
|
+
```bash
|
114
|
+
bin/rails g full_metal_body:install
|
115
|
+
```
|
116
|
+
|
117
|
+
And then execute:
|
118
|
+
|
119
|
+
```bash
|
120
|
+
bin/rails db:migrate
|
121
|
+
```
|
122
|
+
|
123
|
+
The `blocked_actions` table and the `blocked_keys` table will be created in the database.
|
124
|
+
|
125
|
+
### Modify ApplicationController
|
126
|
+
|
127
|
+
Include `FullMetalBody::InputValidationAction` in ApplicationController.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class ApplicationController < ActionController::Base
|
131
|
+
include FullMetalBody::InputValidationAction
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
### Creating a whitelist template
|
136
|
+
|
137
|
+
You can write your own whitelist, but that's a lot of work.
|
138
|
+
|
139
|
+
Therefore, FullMetalBody creates a template whitelist in `tmp/whitelist/**/*.yml` by accessing each action in development mode, and then raises an exception.
|
140
|
+
At that time, the data will be registered in the blocked_actions table and the blocked_keys table.
|
141
|
+
Also, the contents will be output to the log.
|
142
|
+
|
143
|
+
For example, if you access `GET article_path(@article)`, `tmp/whitelist/articles.yml` will be created.
|
144
|
+
|
145
|
+
The contents will look like the following.
|
146
|
+
|
147
|
+
```yaml
|
148
|
+
---
|
149
|
+
articles:
|
150
|
+
show:
|
151
|
+
id:
|
152
|
+
type: string
|
153
|
+
```
|
154
|
+
|
155
|
+
If you copy the contents to `config/whitelist/articles.yml` and then access it again, the exception will not occur.
|
156
|
+
By repeating this process for each action, we can create a whitelist.
|
157
|
+
By default, the whitelist is generated as `string`, so change the type to match the parameters (string|number|date|boolean).
|
158
|
+
|
159
|
+
Parameters will be merged into the template as needed for each action, so you don't have to delete them.
|
160
|
+
For example, if you access `DELETE article_path(@article)` after this, `tmp/whitelist/articles.yml` will look like this
|
161
|
+
|
162
|
+
```yaml
|
163
|
+
---
|
164
|
+
articles:
|
165
|
+
destroy:
|
166
|
+
id:
|
167
|
+
type: string
|
168
|
+
show:
|
169
|
+
id:
|
170
|
+
type: string
|
171
|
+
```
|
172
|
+
|
173
|
+
### If you want to allow all parameters
|
174
|
+
|
175
|
+
When using GraphQL, it is not possible to create a whitelist for the `variables` parameter, since it is defined on the client side.
|
176
|
+
|
177
|
+
In such a case, you need to allow everything under `variables`. If you want to allow them all, specify `_permit_all: true`.
|
178
|
+
|
179
|
+
```yaml
|
180
|
+
---
|
181
|
+
graphql:
|
182
|
+
execute:
|
183
|
+
operationName:
|
184
|
+
type: string
|
185
|
+
query:
|
186
|
+
options:
|
187
|
+
max_length: 1048576
|
188
|
+
type: string
|
189
|
+
variables:
|
190
|
+
_permit_all: true
|
191
|
+
```
|
192
|
+
|
193
|
+
However, although all keys are allowed, to prevent attacks,
|
194
|
+
the type will be inferred from the value and the input value will be validated with the default rules for that type.
|
195
|
+
|
196
|
+
## Development
|
197
|
+
|
198
|
+
Please clone the repository and start development.
|
199
|
+
|
200
|
+
### Preparation
|
201
|
+
|
202
|
+
To develop, start PostgreSQL with docker-compose.
|
203
|
+
|
204
|
+
```bash
|
205
|
+
docker-compose up -d
|
206
|
+
```
|
207
|
+
|
208
|
+
And then execute:
|
209
|
+
|
210
|
+
```bash
|
211
|
+
bundle install
|
212
|
+
```
|
213
|
+
|
214
|
+
### Test
|
215
|
+
|
216
|
+
The test uses minitest.
|
217
|
+
|
218
|
+
```bash
|
219
|
+
bin/test
|
220
|
+
```
|
221
|
+
|
222
|
+
## Contributing
|
223
|
+
|
224
|
+
If you have any bug reports or pull requests, please let me know.
|
225
|
+
|
226
|
+
## License
|
227
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'full_metal_body/input_validation'
|
4
|
+
require 'full_metal_body/services/save_blocked_keys_service'
|
5
|
+
require 'full_metal_body/whitelist_writer'
|
6
|
+
|
7
|
+
module FullMetalBody
|
8
|
+
module InputValidationAction
|
9
|
+
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
RAILS_KEYS = %w(controller action format).map(&:freeze).freeze
|
13
|
+
MAX_BLOCKED_KEYS_COUNT = 3
|
14
|
+
|
15
|
+
included do
|
16
|
+
before_action :validate_params
|
17
|
+
|
18
|
+
unless respond_to?(:current_user, true)
|
19
|
+
class_eval do
|
20
|
+
def current_user
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def validate_params
|
30
|
+
blocked_keys = []
|
31
|
+
whitelist = get_whitelist
|
32
|
+
|
33
|
+
hash_keys(params.to_unsafe_h).each do |key|
|
34
|
+
next if RAILS_KEYS.include?(key.join('.'))
|
35
|
+
|
36
|
+
value = params.dig(*key)
|
37
|
+
if permit_all_params?(key, whitelist)
|
38
|
+
dynamic_whitelist_generator = DynamicWhitelistGenerator.new(key, value, whitelist)
|
39
|
+
whitelist = dynamic_whitelist_generator.execute!
|
40
|
+
# rubocop:disable Lint/UselessAssignment
|
41
|
+
dynamic_whitelist_generator = nil
|
42
|
+
# rubocop:enable Lint/UselessAssignment
|
43
|
+
end
|
44
|
+
|
45
|
+
valid, result = validate_each(key, value, whitelist)
|
46
|
+
if valid
|
47
|
+
if result.nil? && !permit_all_params?(key, whitelist)
|
48
|
+
record_blocked_key(key.join('.'))
|
49
|
+
blocked_keys << key
|
50
|
+
|
51
|
+
next if (ENV['USE_WHITELIST_COUNT_CHECK'] || '1') == '0'
|
52
|
+
|
53
|
+
if blocked_keys.size > MAX_BLOCKED_KEYS_COUNT
|
54
|
+
SaveBlockedKeysService.execute!(controller_path, action_name, blocked_keys)
|
55
|
+
output_error(
|
56
|
+
blocked_keys.to_s,
|
57
|
+
"Unknown parameters existed over #{MAX_BLOCKED_KEYS_COUNT}.",
|
58
|
+
)
|
59
|
+
return nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
else
|
63
|
+
output_error(key.join('.'), result.details)
|
64
|
+
return nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
return if blocked_keys.empty?
|
69
|
+
|
70
|
+
SaveBlockedKeysService.execute!(controller_path, action_name, blocked_keys)
|
71
|
+
|
72
|
+
return unless Rails.env.development?
|
73
|
+
|
74
|
+
WhitelistWriter.new(controller_path, action_name).write!(blocked_keys)
|
75
|
+
raise StandardError, "#{blocked_keys} are not included in whitelist. A template has been created in 'tmp/whitelist/#{controller_path}'"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Validate with whitelist
|
79
|
+
#
|
80
|
+
# @param [Array<String>] key
|
81
|
+
# @param [Object] value
|
82
|
+
# @param [Hash] whitelist
|
83
|
+
#
|
84
|
+
# @return [Boolean] (true, false)
|
85
|
+
# @return [Hash, ActiveModel::Errors] In success: Type definition. In failure: Error infos.
|
86
|
+
def validate_each(key, value, whitelist)
|
87
|
+
key_type = nil
|
88
|
+
(value.nil? ? [nil] : Array(value)).flatten.each do |v|
|
89
|
+
validation = InputValidation.new(key.map(&:to_s), v, whitelist)
|
90
|
+
key_type = validation.key_type
|
91
|
+
return false, validation.errors unless validation.valid?
|
92
|
+
end
|
93
|
+
return true, key_type
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get hash keys recursively.
|
97
|
+
#
|
98
|
+
# @param [Object] obj object
|
99
|
+
# @param [Array<String>] key
|
100
|
+
# @param [Array<String>] result
|
101
|
+
#
|
102
|
+
# @return [Array<String>] result
|
103
|
+
#
|
104
|
+
def hash_keys(obj, key = [], result = [])
|
105
|
+
case obj
|
106
|
+
when Hash
|
107
|
+
obj.each do |k, v|
|
108
|
+
hash_keys(v, key + [k], result)
|
109
|
+
end
|
110
|
+
when Array
|
111
|
+
obj.each_with_index do |v, idx|
|
112
|
+
hash_keys(v, key + [idx], result)
|
113
|
+
end
|
114
|
+
else
|
115
|
+
result << key
|
116
|
+
end
|
117
|
+
result
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Get a whitelist from config/whitelist/**/*.yml
|
122
|
+
#
|
123
|
+
# @return [Hash] Whitelist
|
124
|
+
#
|
125
|
+
def get_whitelist
|
126
|
+
path = Rails.root.join('config', 'whitelist', "#{controller_path}.yml")
|
127
|
+
return nil unless File.exist?(path)
|
128
|
+
|
129
|
+
yaml = File.open(path, "r") do |file|
|
130
|
+
YAML.safe_load(file)&.deep_stringify_keys
|
131
|
+
end
|
132
|
+
yaml&.dig(controller_name, action_name)
|
133
|
+
end
|
134
|
+
|
135
|
+
#
|
136
|
+
# Output validation errors
|
137
|
+
#
|
138
|
+
# @param [String] key
|
139
|
+
# @param [String] errors
|
140
|
+
#
|
141
|
+
def output_error(key, errors)
|
142
|
+
error_message = <<~ERR
|
143
|
+
Input validation error detected
|
144
|
+
Process: #{controller_path}##{action_name}
|
145
|
+
User: #{current_user&.id || 'unknown'}
|
146
|
+
IP: #{request.remote_ip}
|
147
|
+
Key: #{key}
|
148
|
+
Errors: #{errors}
|
149
|
+
ERR
|
150
|
+
# TODO: I want to be able to handle more than just Bugsnag.
|
151
|
+
if Rails.env.production?
|
152
|
+
Bugsnag.notify(error_message) if defined?(Bugsnag)
|
153
|
+
end
|
154
|
+
logger.error error_message
|
155
|
+
head :bad_request
|
156
|
+
end
|
157
|
+
|
158
|
+
# Logging key and process_name when key is not included in whitelist.
|
159
|
+
#
|
160
|
+
# @param [String] key
|
161
|
+
#
|
162
|
+
def record_blocked_key(key)
|
163
|
+
process_name = "#{controller_path}##{action_name}"
|
164
|
+
message = "Input validation warning ('#{key}' not include in whitelist for '#{process_name}')"
|
165
|
+
logger.warn message
|
166
|
+
end
|
167
|
+
|
168
|
+
# Permit all parameters if '_permit_all: true' is existed.
|
169
|
+
#
|
170
|
+
# @param [Array<String,Symbol>] keys
|
171
|
+
# @param [Hash] whitelist
|
172
|
+
# @return [Boolean] (true, false)
|
173
|
+
def permit_all_params?(keys, whitelist)
|
174
|
+
return false if whitelist.blank?
|
175
|
+
|
176
|
+
keys.size.downto(1).any? { |i| whitelist.dig(*keys.first(i))&.fetch('_permit_all', false) }
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FullMetalBody
|
4
|
+
module DeepSort
|
5
|
+
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
refine Hash do
|
9
|
+
def deep_sort
|
10
|
+
keys = self.keys
|
11
|
+
raise DeepSort::Error, "Invalid Keys(#{keys})" unless keys.all? { |k| k.is_a?(String) || k.is_a?(Symbol) || k.is_a?(Numeric) }
|
12
|
+
|
13
|
+
sort.to_h.transform_values { |v| v.respond_to?(:deep_sort) ? v.deep_sort : v }
|
14
|
+
end
|
15
|
+
|
16
|
+
def deep_sort!
|
17
|
+
replace(deep_sort)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
refine Array do
|
22
|
+
def deep_sort
|
23
|
+
case
|
24
|
+
when all?(Numeric) then sort
|
25
|
+
when all?(String), all?(Symbol) then map(&:to_s).sort
|
26
|
+
else
|
27
|
+
map { |v| v.respond_to?(:deep_sort) ? v.deep_sort : v }.sort_by(&:to_s)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def deep_sort!
|
32
|
+
case
|
33
|
+
when all?(Numeric) then sort!
|
34
|
+
when all?(String), all?(Symbol) then map!(&:to_s).sort!
|
35
|
+
else
|
36
|
+
map! { |v| v.respond_to?(:deep_sort!) ? v.deep_sort! : v }.sort_by!(&:to_s)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FullMetalBody
|
4
|
+
class DynamicWhitelistGenerator
|
5
|
+
|
6
|
+
include InputKeyUtils
|
7
|
+
|
8
|
+
# @param [Array<String,Symbol>] keys
|
9
|
+
# @param [Object] value
|
10
|
+
# @param [Hash] whitelist
|
11
|
+
def initialize(keys, value, whitelist = {})
|
12
|
+
@keys = keys
|
13
|
+
@value = value
|
14
|
+
@whitelist = whitelist
|
15
|
+
end
|
16
|
+
|
17
|
+
# Dynamically generate a whitelist and return a merged one.
|
18
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
19
|
+
def execute!
|
20
|
+
if keys_isnt_whitelisted?
|
21
|
+
if keys_include_array?(@keys)
|
22
|
+
generate_whitelist_for_array(@keys)
|
23
|
+
else
|
24
|
+
@whitelist.bury!((@keys + ['type']), type_definition_by_value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
@whitelist.with_indifferent_access
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Check to see if the keys are on the whitelist.
|
33
|
+
# @return [Boolean] (true, false)
|
34
|
+
def keys_isnt_whitelisted?
|
35
|
+
!@whitelist.dig(*@keys)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Check to see if the keys contain the array index.
|
39
|
+
# @return [Boolean] (true, false)
|
40
|
+
def keys_include_array?(keys)
|
41
|
+
!!keys.find_index { |k| key_numeric?(k) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return type-definition of value.
|
45
|
+
# @return [String] Type-definition
|
46
|
+
def type_definition_by_value
|
47
|
+
case @value
|
48
|
+
when Numeric
|
49
|
+
'number'
|
50
|
+
when Date
|
51
|
+
'date'
|
52
|
+
when TrueClass, FalseClass
|
53
|
+
'boolean'
|
54
|
+
else
|
55
|
+
'string'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Dynamically generate an array whitelist.
|
60
|
+
# @param [Array<String,Symbol>] keys
|
61
|
+
# @option [Array<String,Symbol>] prefix_keys
|
62
|
+
# @raise [DynamicWhitelistGenerator::ParseArrayError]
|
63
|
+
def generate_whitelist_for_array(keys, prefix_keys = [])
|
64
|
+
parent_keys, child_keys = separate_by_array_key(keys)
|
65
|
+
parent_keys = prefix_keys + parent_keys
|
66
|
+
@whitelist.bury!((parent_keys + ['type']), 'array')
|
67
|
+
if child_keys.empty?
|
68
|
+
# keys = ["pref_ids", 0]
|
69
|
+
# prefix_keys # => []
|
70
|
+
# parent_keys # => ["pref_ids"]
|
71
|
+
# child_keys # => []
|
72
|
+
# {
|
73
|
+
# "pref_ids" => {
|
74
|
+
# "type" => "array",
|
75
|
+
# "properties" => {
|
76
|
+
# "type" => type_definition_by_value
|
77
|
+
# }
|
78
|
+
# }
|
79
|
+
# }
|
80
|
+
@whitelist.bury!((parent_keys + ['properties', 'type']), type_definition_by_value)
|
81
|
+
elsif child_keys.size == 1 && !key_numeric?(child_keys.first)
|
82
|
+
# keys = ["models", 0, "model_id"]
|
83
|
+
# prefix_keys # => []
|
84
|
+
# parent_keys # => ["models"]
|
85
|
+
# child_keys # => ["model_id"]
|
86
|
+
# {
|
87
|
+
# "models" => {
|
88
|
+
# "type" => "array",
|
89
|
+
# "properties" => {
|
90
|
+
# "model_id" => {
|
91
|
+
# "type" => type_definition_by_value
|
92
|
+
# }
|
93
|
+
# }
|
94
|
+
# }
|
95
|
+
# }
|
96
|
+
@whitelist.bury!((parent_keys + ['properties', child_keys.first, 'type']), type_definition_by_value)
|
97
|
+
elsif keys_include_array?(child_keys)
|
98
|
+
# case 1: 2D array
|
99
|
+
# Lap 1
|
100
|
+
# keys = ["models", 0, 0]
|
101
|
+
# prefix_keys # => []
|
102
|
+
# parent_keys # => ["models"]
|
103
|
+
# child_keys # => [0]
|
104
|
+
# generate_whitelist_for_array([0], ["models", "properties"])
|
105
|
+
# Lap 2
|
106
|
+
# keys = [0]
|
107
|
+
# prefix_keys = ["models", "properties"]
|
108
|
+
# parent_keys = ["models", "properties"]
|
109
|
+
# child_keys = []
|
110
|
+
# Enter `if child_keys.empty?`
|
111
|
+
# {
|
112
|
+
# "models" => {
|
113
|
+
# "type" => "array",
|
114
|
+
# "properties" => {
|
115
|
+
# "type" => "array",
|
116
|
+
# "properties" => {
|
117
|
+
# "type" => type_definition_by_value
|
118
|
+
# }
|
119
|
+
# }
|
120
|
+
# }
|
121
|
+
# }
|
122
|
+
#
|
123
|
+
# case 2: An object in an array has a further array.
|
124
|
+
# Lap 1
|
125
|
+
# keys = ["models", 0, "model_ids", 0]
|
126
|
+
# prefix_keys # => []
|
127
|
+
# parent_keys # => ["models"]
|
128
|
+
# child_keys # => ["model_ids", 0]
|
129
|
+
# generate_whitelist_for_array(["model_ids", 0], ["models", "properties"])
|
130
|
+
# Lap 2
|
131
|
+
# keys = ["model_ids", 0]
|
132
|
+
# prefix_keys = ["models", "properties"]
|
133
|
+
# parent_keys = ["models", "properties", "model_ids"]
|
134
|
+
# child_keys = []
|
135
|
+
# Enter `if child_keys.empty?`
|
136
|
+
# {
|
137
|
+
# "models" => {
|
138
|
+
# "type" => "array",
|
139
|
+
# "properties" => {
|
140
|
+
# "model_ids" => {
|
141
|
+
# "type" => "array",
|
142
|
+
# "properties" => {
|
143
|
+
# "type" => type_definition_by_value
|
144
|
+
# }
|
145
|
+
# }
|
146
|
+
# }
|
147
|
+
# }
|
148
|
+
# }
|
149
|
+
#
|
150
|
+
# case 3: The object in the array has a further array, and the object in the array
|
151
|
+
# Lap 1
|
152
|
+
# keys = ["articles", 0, "comments", 0, "content"]
|
153
|
+
# prefix_keys # => []
|
154
|
+
# parent_keys # => ["articles"]
|
155
|
+
# child_keys # => ["comments", 0, "content"]
|
156
|
+
# generate_whitelist_for_array(["comments", 0, "content"], ["articles", "properties"])
|
157
|
+
# Lap 2
|
158
|
+
# keys = ["comments", 0, "content"]
|
159
|
+
# prefix_keys = ["articles", "properties"]
|
160
|
+
# parent_keys = ["articles", "properties", "comments"]
|
161
|
+
# child_keys = ["content"]
|
162
|
+
# Enter `elsif child_keys.size == 1 && !key_numeric?(child_keys.first)`
|
163
|
+
# {
|
164
|
+
# "articles" => {
|
165
|
+
# "type" => "array",
|
166
|
+
# "properties" => {
|
167
|
+
# "comments" => {
|
168
|
+
# "type" => "array",
|
169
|
+
# "properties" => {
|
170
|
+
# "content" => {
|
171
|
+
# "type" => type_definition_by_value
|
172
|
+
# }
|
173
|
+
# }
|
174
|
+
# }
|
175
|
+
# }
|
176
|
+
# }
|
177
|
+
# }
|
178
|
+
generate_whitelist_for_array(child_keys, parent_keys + ['properties'])
|
179
|
+
else
|
180
|
+
# Normally, there are no cases that come here.
|
181
|
+
# But if it should come, raise Exception.
|
182
|
+
invalid_keys = prefix_keys + keys
|
183
|
+
invalid_keys.delete('properties')
|
184
|
+
raise DynamicWhitelistGenerator::ParseArrayError.new("Invalid keys", invalid_keys)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class ParseArrayError < StandardError
|
189
|
+
|
190
|
+
# @param [String] message
|
191
|
+
# @param [Array<String,Symbol>] keys
|
192
|
+
def initialize(message, keys)
|
193
|
+
@keys = keys
|
194
|
+
super("#{message}: #{@keys}")
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|