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