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
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FullMetalBody
|
4
|
+
module InputKeyUtils
|
5
|
+
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# Check if key is a numeric.
|
11
|
+
# @param [String,Integer] key
|
12
|
+
# @return [Boolean] (true, false)
|
13
|
+
def key_numeric?(key)
|
14
|
+
key.to_i.to_s == key || key.is_a?(Numeric)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Divide the keys by the number representing the array.
|
18
|
+
# @param [Array<String,Symbol>] keys
|
19
|
+
# @return [Array<Array>] parent_keys and child_keys
|
20
|
+
def separate_by_array_key(keys)
|
21
|
+
idx = keys.find_index { |k| key_numeric?(k) }
|
22
|
+
parent_keys, child_keys = keys.partition.with_index { |_, i| i <= idx }
|
23
|
+
parent_keys.delete_at(-1) # parent_keys.last is unnecessary.
|
24
|
+
return parent_keys, child_keys
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'full_metal_body/input_key_utils'
|
4
|
+
require 'full_metal_body/internal/input_file_validator'
|
5
|
+
require 'full_metal_body/internal/input_string_validator'
|
6
|
+
require 'full_metal_body/internal/reasonable_boolean_validator'
|
7
|
+
require 'full_metal_body/internal/reasonable_date_validator'
|
8
|
+
|
9
|
+
module FullMetalBody
|
10
|
+
class InputValidation
|
11
|
+
|
12
|
+
include InputKeyUtils
|
13
|
+
include ActiveModel::Validations
|
14
|
+
|
15
|
+
attr_accessor :key, :value, :key_type
|
16
|
+
|
17
|
+
validates :key, "full_metal_body/internal/input_string": true, if: :check_key?
|
18
|
+
|
19
|
+
# @param [Array<String, Symbol>] key
|
20
|
+
# @param [Object] value
|
21
|
+
# @param [Hash] whitelist
|
22
|
+
def initialize(key, value, whitelist)
|
23
|
+
@key = key
|
24
|
+
@value = value
|
25
|
+
set_key_type(whitelist)
|
26
|
+
if value_validate
|
27
|
+
class_eval %(
|
28
|
+
validates :value, #{value_validate} # validates :value, input_string: true
|
29
|
+
), __FILE__, __LINE__ - 2
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Whether to validate the key
|
36
|
+
#
|
37
|
+
# @return [Boolean] (true, false)
|
38
|
+
def check_key?
|
39
|
+
!@key_type
|
40
|
+
end
|
41
|
+
|
42
|
+
# Which validator to validate the value with
|
43
|
+
#
|
44
|
+
# @return [String] validator type
|
45
|
+
def value_validate
|
46
|
+
if @key_type.nil?
|
47
|
+
if @value.is_a?(ActionDispatch::Http::UploadedFile)
|
48
|
+
return "'full_metal_body/internal/input_file': true"
|
49
|
+
end
|
50
|
+
|
51
|
+
return "'full_metal_body/internal/input_string': true"
|
52
|
+
end
|
53
|
+
|
54
|
+
case @key_type['type']
|
55
|
+
when 'string'
|
56
|
+
options = @key_type['options']&.symbolize_keys || true
|
57
|
+
"'full_metal_body/internal/input_string': #{options}"
|
58
|
+
when 'number'
|
59
|
+
options = @key_type['options']&.symbolize_keys || { allow_blank: true }
|
60
|
+
if @value.is_a?(Numeric)
|
61
|
+
"numericality: #{options}"
|
62
|
+
else
|
63
|
+
"'full_metal_body/internal/input_string': true, numericality: #{options}"
|
64
|
+
end
|
65
|
+
when 'date'
|
66
|
+
if @value.is_a?(Date)
|
67
|
+
"'full_metal_body/internal/reasonable_date': true"
|
68
|
+
else
|
69
|
+
"'full_metal_body/internal/input_string': true, 'full_metal_body/internal/reasonable_date': true"
|
70
|
+
end
|
71
|
+
when 'file'
|
72
|
+
options = @key_type['options']&.symbolize_keys || true
|
73
|
+
"'full_metal_body/internal/input_file': #{options}"
|
74
|
+
when 'boolean'
|
75
|
+
options = @key_type['options']&.symbolize_keys || true
|
76
|
+
if [TrueClass, FalseClass].any? { |klass| @value.is_a?(klass) }
|
77
|
+
"'full_metal_body/internal/reasonable_boolean': #{options}"
|
78
|
+
else
|
79
|
+
"'full_metal_body/internal/input_string': true, 'full_metal_body/internal/reasonable_boolean': #{options}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Set type-definition to @key_type
|
85
|
+
#
|
86
|
+
# @param [Hash] whitelist
|
87
|
+
def set_key_type(whitelist)
|
88
|
+
return @key_type = nil if whitelist.nil?
|
89
|
+
|
90
|
+
# When ype-definition by key is exists in whitelist
|
91
|
+
return @key_type = whitelist.dig(*@key) if has_type?(whitelist.dig(*@key))
|
92
|
+
|
93
|
+
# When @key has the index number of the array
|
94
|
+
if @key.find_index { |k| key_numeric?(k) }
|
95
|
+
result = get_key_type_recursively(@key, whitelist)
|
96
|
+
return @key_type = result if has_type?(result)
|
97
|
+
end
|
98
|
+
|
99
|
+
# When no type-definition by key in whitelist
|
100
|
+
@key_type = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# Search type-definition by key recursively from whitelist
|
104
|
+
#
|
105
|
+
# @param [Array<String>] keys
|
106
|
+
# @param [Hash] whitelist
|
107
|
+
#
|
108
|
+
# @return [Hash, NilClass] type-definition by key
|
109
|
+
def get_key_type_recursively(keys, whitelist)
|
110
|
+
parent_keys, child_keys = separate_by_array_key(keys)
|
111
|
+
parent = parent_keys.empty? ? whitelist : whitelist.dig(*parent_keys)
|
112
|
+
|
113
|
+
if parent['type'] == 'array'
|
114
|
+
if child_keys.empty?
|
115
|
+
# { type: array, properties: { type: string } }
|
116
|
+
parent['properties']
|
117
|
+
elsif parent['properties'].dig(*child_keys)
|
118
|
+
# { type: array, properties: { hoge: { type: string } } }
|
119
|
+
parent['properties'].dig(*child_keys)
|
120
|
+
elsif parent.dig('properties', 'type') == 'array'
|
121
|
+
# { type: array, properties: { type: array, properties: { ... } } }
|
122
|
+
get_key_type_recursively(child_keys, parent['properties'])
|
123
|
+
elsif parent.dig('properties', child_keys.first, 'type') == 'array'
|
124
|
+
# { type: array, properties: { hoge: { type: array, properties: { ... } } } }
|
125
|
+
first_key = child_keys.shift
|
126
|
+
get_key_type_recursively(child_keys, parent.dig('properties', first_key))
|
127
|
+
else
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
else
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Whether or not the item has a type attribute.
|
136
|
+
#
|
137
|
+
# @param [Object] item
|
138
|
+
#
|
139
|
+
# @return [Boolean] (true, false)
|
140
|
+
def has_type?(item)
|
141
|
+
return false unless item.is_a?(Hash)
|
142
|
+
|
143
|
+
%w(string number date file boolean).include?(item['type'])
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
module FullMetalBody
|
3
|
+
module Internal
|
4
|
+
class InputFileValidator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
DEFAULT_CONTENT_TYPES = '*'.freeze
|
7
|
+
DEFAULT_FILE_SIZE = 100.megabytes
|
8
|
+
|
9
|
+
def check_validity!
|
10
|
+
if options[:content_type]
|
11
|
+
value = options[:content_type]
|
12
|
+
unless value.is_a?(String) || (value.is_a?(Array) && value.all?(String))
|
13
|
+
raise ArgumentError, ":content_type must be String or Array<String>"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
if options[:file_size]
|
17
|
+
value = options[:file_size]
|
18
|
+
raise ArgumentError, ":file_size must be a non-negative Integer" unless value.is_a?(Integer) && value >= 0
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_each(record, attribute, value)
|
23
|
+
return if value.nil?
|
24
|
+
|
25
|
+
content_types = options[:content_type] || DEFAULT_CONTENT_TYPES
|
26
|
+
file_size = options[:file_size] || DEFAULT_FILE_SIZE
|
27
|
+
|
28
|
+
# type
|
29
|
+
unless value.is_a? ActionDispatch::Http::UploadedFile
|
30
|
+
return record.errors.add(attribute, :file_wrong_type, value: value)
|
31
|
+
end
|
32
|
+
|
33
|
+
# content type
|
34
|
+
if content_types != '*' && Array(content_types).exclude?(value.content_type)
|
35
|
+
return record.errors.add(attribute, :invalid_content_type)
|
36
|
+
end
|
37
|
+
|
38
|
+
# file size
|
39
|
+
if value.size > file_size
|
40
|
+
record.errors.add(attribute, :too_large_size)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module FullMetalBody
|
2
|
+
module Internal
|
3
|
+
|
4
|
+
class InputStringValidator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
DEFAULT_MAX_LENGTH = 1024
|
7
|
+
|
8
|
+
def check_validity!
|
9
|
+
if options[:max_length]
|
10
|
+
value = options[:max_length]
|
11
|
+
raise ArgumentError, ":max_length must be a non-negative Integer" unless value.is_a?(Integer) && value >= 0
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate_each(record, attribute, value)
|
16
|
+
return if value.nil?
|
17
|
+
|
18
|
+
if value.is_a?(Array)
|
19
|
+
value.each do |v|
|
20
|
+
validate_value(record, attribute, v.dup)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
validate_value(record, attribute, value.dup)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate_value(record, attribute, value)
|
28
|
+
max_length = options[:max_length] || DEFAULT_MAX_LENGTH
|
29
|
+
|
30
|
+
# type
|
31
|
+
unless value.is_a? String
|
32
|
+
return record.errors.add(attribute, :wrong_type, value: value)
|
33
|
+
end
|
34
|
+
|
35
|
+
# length
|
36
|
+
if value.size > max_length
|
37
|
+
return record.errors.add(attribute, :too_long, value: byteslice(value), count: value.size)
|
38
|
+
end
|
39
|
+
|
40
|
+
# encoding
|
41
|
+
original_encoding = value.encoding.name
|
42
|
+
unless value.force_encoding('UTF-8').valid_encoding?
|
43
|
+
return record.errors.add(attribute, :wrong_encoding, value: byteslice(value), encoding: original_encoding)
|
44
|
+
end
|
45
|
+
|
46
|
+
# cntrl
|
47
|
+
# Replace and delete line feed codes and horizontal tabs (vertical tabs are not)
|
48
|
+
value = value.gsub(/\R|\t/, '')
|
49
|
+
if /[[:cntrl:]]/.match?(value)
|
50
|
+
record.errors.add(attribute, :include_cntrl, value: byteslice(value))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def byteslice(value)
|
55
|
+
value.byteslice(0, 1024)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module FullMetalBody
|
2
|
+
module Internal
|
3
|
+
class ReasonableBooleanValidator < ActiveModel::EachValidator
|
4
|
+
|
5
|
+
TRUE_VALUES = [true, "1", "t", "T", "true", "TRUE", "on", "ON"].to_set.freeze
|
6
|
+
FALSE_VALUES = [false, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set.freeze
|
7
|
+
|
8
|
+
def validate_each(record, attribute, value)
|
9
|
+
unless [TRUE_VALUES, FALSE_VALUES].any? { |b| b.include?(value) }
|
10
|
+
record.errors.add(attribute, :not_reasonable_boolean, value: value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FullMetalBody
|
2
|
+
module Internal
|
3
|
+
|
4
|
+
class ReasonableDateValidator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
def validate_each(record, attribute, value)
|
7
|
+
return if value.blank? || value.is_a?(Date)
|
8
|
+
|
9
|
+
unless date_valid?(value)
|
10
|
+
record.errors.add(attribute, :not_reasonable_date, value: value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def date_valid?(str)
|
15
|
+
# Allow only slash and hyphen separators
|
16
|
+
unless str.match?(%r{^\d{4}/\d{1,2}/\d{1,2}$|^\d{4}-\d{1,2}-\d{1,2}$})
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
|
20
|
+
!!Date.parse(str)
|
21
|
+
rescue
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FullMetalBody
|
4
|
+
class BlockedAction < ActiveRecord::Base
|
5
|
+
|
6
|
+
has_many :blocked_keys, dependent: :destroy
|
7
|
+
|
8
|
+
validates :controller, presence: true, uniqueness: { scope: :action }
|
9
|
+
validates :action, presence: true
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FullMetalBody
|
4
|
+
class SaveBlockedKeysService
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Save blocked_keys to the database using bulk insert.
|
9
|
+
#
|
10
|
+
# @param [String] controller_path
|
11
|
+
# @param [String] action_name
|
12
|
+
# @param [Array<Array<String>>] blocked_keys
|
13
|
+
# @return [ActiveRecord::Result]
|
14
|
+
def execute!(controller_path, action_name, blocked_keys)
|
15
|
+
ApplicationRecord.transaction do
|
16
|
+
blocked_action = BlockedAction.find_or_create_by!(controller: controller_path, action: action_name)
|
17
|
+
now = Time.zone.now
|
18
|
+
if rails_6_1_and_up?
|
19
|
+
attributes = blocked_keys.map { |key| { blocked_key: key } }
|
20
|
+
blocked_action.blocked_keys.create_with(
|
21
|
+
created_at: now,
|
22
|
+
updated_at: now,
|
23
|
+
).insert_all(attributes, returning: [:id], unique_by: [:blocked_action_id, :blocked_key])
|
24
|
+
elsif rails_6_0?
|
25
|
+
attributes = blocked_keys.map do |key|
|
26
|
+
{
|
27
|
+
blocked_action_id: blocked_action.id,
|
28
|
+
blocked_key: key,
|
29
|
+
created_at: now,
|
30
|
+
updated_at: now,
|
31
|
+
}
|
32
|
+
end
|
33
|
+
blocked_action.blocked_keys.insert_all(
|
34
|
+
attributes,
|
35
|
+
returning: [:id],
|
36
|
+
unique_by: [:blocked_action_id, :blocked_key],
|
37
|
+
)
|
38
|
+
else
|
39
|
+
blocked_keys.each { |key| blocked_action.blocked_keys.find_or_create_by(blocked_key: key) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def rails_6_1_and_up?
|
47
|
+
Gem::Version.new(Rails.version) >= Gem::Version.new("6.1.0")
|
48
|
+
end
|
49
|
+
|
50
|
+
def rails_6_0?
|
51
|
+
Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 0
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'full_metal_body/deep_sort'
|
5
|
+
require 'full_metal_body/dynamic_whitelist_generator'
|
6
|
+
using FullMetalBody::DeepSort
|
7
|
+
|
8
|
+
module FullMetalBody
|
9
|
+
|
10
|
+
class WhitelistWriter
|
11
|
+
|
12
|
+
# @param [String] controller_path
|
13
|
+
# @param [String] action_name
|
14
|
+
def initialize(controller_path, action_name)
|
15
|
+
@controller_path = controller_path
|
16
|
+
@action_name = action_name
|
17
|
+
@data = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [Array<Array>] blocked_keys
|
21
|
+
def write!(blocked_keys)
|
22
|
+
blocked_keys.each { |key| update_data!(key) }
|
23
|
+
|
24
|
+
dir = File.dirname(save_path)
|
25
|
+
FileUtils.mkdir_p(dir, mode: 0o777) unless Dir.exist?(dir)
|
26
|
+
if File.exist?(save_path)
|
27
|
+
whitelist = YAML.load_file(save_path)
|
28
|
+
@data.deep_merge!(whitelist)
|
29
|
+
end
|
30
|
+
@data.deep_sort!
|
31
|
+
File.open(save_path, "w") do |file|
|
32
|
+
YAML.dump(@data, file)
|
33
|
+
end
|
34
|
+
FileUtils.chmod(0o777, save_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return path to store the whitelist.
|
38
|
+
# @return [Pathname]
|
39
|
+
def save_path
|
40
|
+
@save_path ||= if Rails.env.test?
|
41
|
+
Rails.root.join('tmp', "test#{ENV.fetch('TEST_ENV_NUMBER', '')}", 'whitelist', "#{@controller_path}.yml")
|
42
|
+
else
|
43
|
+
Rails.root.join('tmp', 'whitelist', "#{@controller_path}.yml")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :action_name
|
50
|
+
|
51
|
+
def controller_name
|
52
|
+
@controller_path.split('/').last
|
53
|
+
end
|
54
|
+
|
55
|
+
# Dynamically generate whitelist from keys.
|
56
|
+
# @param [Array] keys
|
57
|
+
def update_data!(keys)
|
58
|
+
whitelist = DynamicWhitelistGenerator.new(keys, 'string').execute!
|
59
|
+
@data.bury!([controller_name, action_name], whitelist.to_hash)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require "full_metal_body/version"
|
2
|
+
require "full_metal_body/railtie"
|
3
|
+
require "bury"
|
4
|
+
|
5
|
+
module FullMetalBody
|
6
|
+
require 'full_metal_body/models/blocked_action'
|
7
|
+
require 'full_metal_body/models/blocked_key'
|
8
|
+
require 'full_metal_body/controllers/concerns/input_validation_action'
|
9
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FullMetalBody
|
4
|
+
class InstallGenerator < ::Rails::Generators::Base
|
5
|
+
include ::Rails::Generators::Migration
|
6
|
+
|
7
|
+
source_root File.expand_path('templates', __dir__)
|
8
|
+
|
9
|
+
def self.next_migration_number(_path)
|
10
|
+
Time.now.utc.strftime('%Y%m%d%H%M%S')
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_migration_file
|
14
|
+
template = 'create_blocked_actions'
|
15
|
+
migration_dir = File.expand_path('db/migrate')
|
16
|
+
migration_template(
|
17
|
+
"#{template}.rb.erb",
|
18
|
+
"db/migrate/#{template}.rb",
|
19
|
+
migration_version: migration_version,
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_whitelist_dir
|
24
|
+
empty_directory('config/whitelist')
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def migration_version
|
30
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if over_rails5?
|
31
|
+
end
|
32
|
+
|
33
|
+
def over_rails5?
|
34
|
+
Rails::VERSION::MAJOR >= 5
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateBlockedActions < ActiveRecord::Migration<%= migration_version %>
|
4
|
+
|
5
|
+
def change
|
6
|
+
create_table :blocked_actions do |t|
|
7
|
+
t.string :controller, null: false
|
8
|
+
t.string :action, null: false
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
add_index :blocked_actions, [:controller, :action], unique: true
|
13
|
+
|
14
|
+
create_table :blocked_keys do |t|
|
15
|
+
t.references :blocked_action, foreign_key: true, null: false
|
16
|
+
t.string :blocked_key, array: true, null: false
|
17
|
+
|
18
|
+
t.timestamps
|
19
|
+
end
|
20
|
+
add_index :blocked_keys, [:blocked_action_id, :blocked_key], unique: true
|
21
|
+
end
|
22
|
+
end
|