Skip to content

Commit 4a07553

Browse files
committed
Merge PR #45660
2 parents 2434e20 + e313fc5 commit 4a07553

File tree

4 files changed

+79
-8
lines changed

4 files changed

+79
-8
lines changed

activerecord/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Allow per attribute setting of YAML permitted classes (safe load) and unsafe load.
2+
3+
*Carlos Palhares*
4+
15
* Add a build persistence method
26

37
Provides a wrapper for `new`, to provide feature parity with `create`s

activerecord/lib/active_record/attribute_methods/serialization.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ module ClassMethods
5151
# using the coder's <tt>dump(value)</tt> method, and will be
5252
# deserialized using the coder's <tt>load(string)</tt> method. The
5353
# +dump+ method may return +nil+ to serialize the value as +NULL+.
54+
# * +yaml+ - Optional. Yaml specific options. The allowed config is:
55+
# * +:permitted_classes+ - +Array+ with the permitted classes.
56+
# * +:unsafe_load+ - Unsafely load YAML blobs, allow YAML to load any class.
5457
#
5558
# ==== Options
5659
#
@@ -78,6 +81,12 @@ module ClassMethods
7881
# serialize :preferences, Hash
7982
# end
8083
#
84+
# ===== Serializes +preferences+ to YAML, permitting select classes
85+
#
86+
# class User < ActiveRecord::Base
87+
# serialize :preferences, yaml: { permitted_classes: [Symbol, Time] }
88+
# end
89+
#
8190
# ===== Serialize the +preferences+ attribute using a custom coder
8291
#
8392
# class Rot13JSON
@@ -100,7 +109,7 @@ module ClassMethods
100109
# serialize :preferences, Rot13JSON
101110
# end
102111
#
103-
def serialize(attr_name, class_name_or_coder = Object, **options)
112+
def serialize(attr_name, class_name_or_coder = Object, yaml: {}, **options)
104113
# When ::JSON is used, force it to go through the Active Support JSON encoder
105114
# to ensure special objects (e.g. Active Record models) are dumped correctly
106115
# using the #as_json hook.
@@ -109,7 +118,7 @@ def serialize(attr_name, class_name_or_coder = Object, **options)
109118
elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
110119
class_name_or_coder
111120
else
112-
Coders::YAMLColumn.new(attr_name, class_name_or_coder)
121+
Coders::YAMLColumn.new(attr_name, class_name_or_coder, **yaml)
113122
end
114123

115124
attribute(attr_name, **options) do |cast_type|

activerecord/lib/active_record/coders/yaml_column.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ module Coders # :nodoc:
77
class YAMLColumn # :nodoc:
88
attr_accessor :object_class
99

10-
def initialize(attr_name, object_class = Object)
10+
def initialize(attr_name, object_class = Object, permitted_classes: [], unsafe_load: nil)
1111
@attr_name = attr_name
1212
@object_class = object_class
13+
@permitted_classes = permitted_classes
14+
@unsafe_load = unsafe_load
1315
check_arity_of_constructor
1416
end
1517

@@ -39,6 +41,14 @@ def assert_valid_value(obj, action:)
3941
end
4042

4143
private
44+
def permitted_classes
45+
[*ActiveRecord.yaml_column_permitted_classes, *@permitted_classes]
46+
end
47+
48+
def unsafe_load?
49+
@unsafe_load.nil? ? ActiveRecord.use_yaml_unsafe_load : @unsafe_load
50+
end
51+
4252
def check_arity_of_constructor
4353
load(nil)
4454
rescue ArgumentError
@@ -47,18 +57,18 @@ def check_arity_of_constructor
4757

4858
if YAML.respond_to?(:unsafe_load)
4959
def yaml_load(payload)
50-
if ActiveRecord.use_yaml_unsafe_load
60+
if unsafe_load?
5161
YAML.unsafe_load(payload)
5262
else
53-
YAML.safe_load(payload, permitted_classes: ActiveRecord.yaml_column_permitted_classes, aliases: true)
63+
YAML.safe_load(payload, permitted_classes: permitted_classes, aliases: true)
5464
end
5565
end
5666
else
5767
def yaml_load(payload)
58-
if ActiveRecord.use_yaml_unsafe_load
68+
if unsafe_load?
5969
YAML.load(payload)
6070
else
61-
YAML.safe_load(payload, permitted_classes: ActiveRecord.yaml_column_permitted_classes, aliases: true)
71+
YAML.safe_load(payload, permitted_classes: permitted_classes, aliases: true)
6272
end
6373
end
6474
end

activerecord/test/cases/coders/yaml_column_test.rb

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ module ActiveRecord
66
module Coders
77
class YAMLColumnTest < ActiveRecord::TestCase
88
setup do
9+
@use_yaml_unsafe_load = ActiveRecord.use_yaml_unsafe_load
910
ActiveRecord.use_yaml_unsafe_load = true
1011
end
1112

13+
teardown do
14+
ActiveRecord.use_yaml_unsafe_load = @use_yaml_unsafe_load
15+
end
16+
1217
def test_initialize_takes_class
1318
coder = YAMLColumn.new("attr_name", Object)
1419
assert_equal Object, coder.object_class
@@ -69,10 +74,16 @@ def test_load_doesnt_handle_undefined_class_or_module
6974

7075
class YAMLColumnTestWithSafeLoad < YAMLColumnTest
7176
setup do
77+
@use_yaml_unsafe_load = ActiveRecord.use_yaml_unsafe_load
7278
@yaml_column_permitted_classes_default = ActiveRecord.yaml_column_permitted_classes
7379
ActiveRecord.use_yaml_unsafe_load = false
7480
end
7581

82+
teardown do
83+
ActiveRecord.use_yaml_unsafe_load = @use_yaml_unsafe_load
84+
ActiveRecord.yaml_column_permitted_classes = @yaml_column_permitted_classes_default
85+
end
86+
7687
def test_yaml_column_permitted_classes_are_consumed_by_safe_load
7788
ActiveRecord.yaml_column_permitted_classes = [Symbol, Time]
7889

@@ -84,8 +95,45 @@ def test_yaml_column_permitted_classes_are_consumed_by_safe_load
8495
coder.load(time_yaml)
8596
coder.load(symbol_yaml)
8697
end
98+
end
8799

88-
ActiveRecord.yaml_column_permitted_classes = @yaml_column_permitted_classes_default
100+
def test_yaml_column_permitted_classes_option
101+
ActiveRecord.yaml_column_permitted_classes = [Symbol]
102+
103+
coder = YAMLColumn.new("attr_name", permitted_classes: [Time])
104+
time_yaml = YAML.dump(Time.new)
105+
symbol_yaml = YAML.dump(:somesymbol)
106+
107+
assert_nothing_raised do
108+
coder.load(time_yaml)
109+
coder.load(symbol_yaml)
110+
end
111+
end
112+
113+
def test_yaml_column_unsafe_load_option
114+
ActiveRecord.use_yaml_unsafe_load = false
115+
ActiveRecord.yaml_column_permitted_classes = []
116+
117+
coder = YAMLColumn.new("attr_name", unsafe_load: true)
118+
time_yaml = YAML.dump(Time.new)
119+
symbol_yaml = YAML.dump(:somesymbol)
120+
121+
assert_nothing_raised do
122+
coder.load(time_yaml)
123+
coder.load(symbol_yaml)
124+
end
125+
end
126+
127+
def test_yaml_column_override_unsafe_load_option
128+
ActiveRecord.use_yaml_unsafe_load = true
129+
ActiveRecord.yaml_column_permitted_classes = []
130+
131+
coder = YAMLColumn.new("attr_name", unsafe_load: false)
132+
time_yaml = YAML.dump(Time.new)
133+
134+
assert_raises(Psych::DisallowedClass) do
135+
coder.load(time_yaml)
136+
end
89137
end
90138

91139
def test_load_doesnt_handle_undefined_class_or_module

0 commit comments

Comments
 (0)