Skip to content

Commit

Permalink
Merge pull request #5 from fursich/feature/yaml-premitted-classes
Browse files Browse the repository at this point in the history
Feature/yaml premitted classes
  • Loading branch information
fursich authored Oct 1, 2024
2 parents 97c18c0 + 3366b0c commit af96c2d
Show file tree
Hide file tree
Showing 12 changed files with 822 additions and 499 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,48 @@ DumpedRailers.import!(fixture_path, before_save: before_callback, after_save: [a

`before_save` / `after_save` can accept both single and multiple (array) arguments.

### Deserializing Custom Classes with YAML

* YAML (Psych) does not permit to load random class objects for [security reasons](https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017).
* By default, DumpedRailers handles all the objects that Rails permitts (i.e. [ActiveRecord.yaml_column_permitted_classes](https://guides.rubyonrails.org/configuring.html#config-active-record-yaml-column-permitted-classes)), plus Time, Date, and DateTime.
* DumpedRailers raises `Psych::DisallowedClass` error when non-permitted classes are detected. If you want DumpedRailsers handle other classes, you could specify `yaml_column_permitted_classes` option with configurations or import method's arguments.
* *Please use this option with extra care* for security - again, it is recommended to use this for development purpose only.

```ruby
DumpedRailers.configure do |config|
config.ignorable_columns += [:published_on] # :published_on will be ignored *on top of* default settings.
end
```

#### Caveats
* If you wish to load Date, Time object, it would be easier to load it as a string. DumpedRailers will pass it to the specified ActiveRecord models and they typecast the raw string into the appropreate date/time object.

* below columns (published_date, published_time, first_drafted_at) all will be passed as a string (as the value is surrounded by the quotes). Strings will be interperted to apropreate column type with ActiveRecord.

```ruby
_fixture:
model_class: Article
fixture_generated_by: DumpedRailers
__article_1:
title: Harry Potter
published_date: '2024-03-01'
published_time: '10:00:00'
first_drafted_at: '2024-02-01T10:10:10+09:00'
```

* below fixture (without quotes) will be directly interperted to Date or Time via YAML module. It needs to have proper format that YAML can interpret.

```ruby
_fixture:
model_class: Article
fixture_generated_by: DumpedRailers
__article_1:
title: Harry Potter
published_date: 2024-03-01
published_time: 2000-01-01 10:00:00
first_drafted_at: 2024-02-01T10:10:10+09:00
```

### Configuration

* All the settings can be configured by either configuration (global) or arguments (at runtime).
Expand Down
13 changes: 10 additions & 3 deletions lib/dumped_railers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@ def dump!(*models, base_dir: nil, preprocessors: nil, ignorable_columns: nil)
fixtures
end

def import!(*paths, authorized_models: nil, before_save: nil, after_save: nil)
def import!(*paths, authorized_models: nil, before_save: nil, after_save: nil, yaml_column_permitted_classes: [])
# make sure class-baseed caches starts with clean state
DumpedRailers::RecordBuilder::FixtureRow::RecordStore.clear!
DumpedRailers::RecordBuilder::DependencyTracker.clear!

# override global config settings when options are specified
runtime_options = { authorized_models: authorized_models.presence }.compact.reverse_merge(import_options)
runtime_options =
{
authorized_models: authorized_models.presence,
yaml_column_permitted_classes: yaml_column_permitted_classes.presence,
}
.compact
.reverse_merge(import_options)

before_save = Array(before_save).compact
after_save = Array(after_save).compact
Expand All @@ -40,6 +46,7 @@ def import!(*paths, authorized_models: nil, before_save: nil, after_save: nil)
authorized_models: runtime_options[:authorized_models],
before_save: before_save,
after_save: after_save,
yaml_column_permitted_classes: runtime_options[:yaml_column_permitted_classes]
)
fixture_handler.import_all!
end
Expand All @@ -55,7 +62,7 @@ def dump_options
end

def import_options
options.slice(:authorized_models)
options.slice(:authorized_models, :yaml_column_permitted_classes)
end
end

Expand Down
14 changes: 12 additions & 2 deletions lib/dumped_railers/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
module DumpedRailers
module Configuration
extend Forwardable
def_delegators :@_config, :preprocessors, :ignorable_columns, :authorized_models
def_delegators :@_config, :preprocessors, :ignorable_columns, :authorized_models, :yaml_column_permitted_classes

def configure
yield config
Expand All @@ -17,10 +17,20 @@ def options

IGNORABLE_COLUMNS = %w[id created_at updated_at]
def configure_defaults!
default_yaml_column_permitted_classes =
# FIXME: this will be no longer needed when we drop support for older Rails versions
# https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017
if ActiveRecord.respond_to?(:yaml_column_permitted_classes)
ActiveRecord.yaml_column_permitted_classes + [Date, Time, DateTime]
else
[Date, Time, DateTime]
end

clear_configuration!(
ignorable_columns: IGNORABLE_COLUMNS,
preprocessors: [],
preprocessors: [],
authorized_models: :any,
yaml_column_permitted_classes: default_yaml_column_permitted_classes,
)
end

Expand Down
4 changes: 2 additions & 2 deletions lib/dumped_railers/file_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
module DumpedRailers
module FileHelper
class << self
def read_fixtures(*paths)
def read_fixtures(*paths, yaml_column_permitted_classes: [])
yaml_files = paths.flat_map { |path|
if File.file?(path)
path
Expand All @@ -18,7 +18,7 @@ def read_fixtures(*paths)

yaml_files.map { |file|
raw_data = ::File.read(file)
YAML.load(raw_data)
YAML.safe_load(raw_data, permitted_classes: yaml_column_permitted_classes)
}
end

Expand Down
4 changes: 2 additions & 2 deletions lib/dumped_railers/import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ module DumpedRailers
class Import
attr_reader :fixture_set

def initialize(*paths, authorized_models: [], before_save: [], after_save: [])
def initialize(*paths, authorized_models: [], before_save: [], after_save: [], yaml_column_permitted_classes: [])
@before_save = before_save
@after_save = after_save

if (paths.first.is_a? Hash)
@raw_fixtures = paths.first.values
else
@raw_fixtures = FileHelper.read_fixtures(*paths)
@raw_fixtures = FileHelper.read_fixtures(*paths, yaml_column_permitted_classes: yaml_column_permitted_classes)
end

@fixture_set = RecordBuilder::FixtureSet.new(@raw_fixtures, authorized_models: authorized_models)
Expand Down
9 changes: 9 additions & 0 deletions spec/fixtures/articles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@ _fixture:
__article_36:
title: Phoenix
writer: __author_5
published_date: 2024-04-01
published_time: '10:30:00'
first_drafted_at: 2024-02-01T12:12:12
__article_52:
title: Harry Potter
writer: __author_4
published_date: 2024-03-01
published_time: '10:00:00'
first_drafted_at: 2024-02-01T10:10:10+09:00
__article_143:
title: Princess Mononoke
writer: __author_13
published_date: 2024-05-01
published_time: '09:00:00'
first_drafted_at: 2024-02-01T08:08:08-05:00
__article_88:
title: Alice in Wonderland
writer: __author_83
Expand Down
21 changes: 21 additions & 0 deletions spec/lib/dumped_railers/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
config.ignorable_columns = [:uuid]
config.preprocessors = [:foo, :bar]
config.authorized_models = [:model1, :model2]
config.yaml_column_permitted_classes = [Date]
config.a_random_option = 'something'
end
end
Expand All @@ -110,6 +111,7 @@
ignorable_columns: [:uuid],
preprocessors: [:foo, :bar],
authorized_models: [:model1, :model2],
yaml_column_permitted_classes: [Date],
a_random_option: 'something',
)
}
Expand All @@ -118,6 +120,7 @@
subject {
klass.options[:ignorable_columns] << :published_at
klass.options[:preprocessors] << :baz
klass.options[:yaml_column_permitted_classes] << Time
}

it 'does updates original configurations' do
Expand All @@ -126,6 +129,7 @@
ignorable_columns: [:uuid, :published_at],
preprocessors: [:foo, :bar, :baz],
authorized_models: [:model1, :model2],
yaml_column_permitted_classes: [Date, Time],
a_random_option: 'something',
}
)
Expand All @@ -141,10 +145,17 @@
config.ignorable_columns = [:uuid]
config.preprocessors = [:foo, :bar]
config.authorized_models = [:model1, :model2]
config.yaml_column_permitted_classes = [Date, Time]
config.a_random_option = :something
end
end

it 'has preset options' do
expect { subject }.to change { klass.options.keys }.to contain_exactly(
*%i[ignorable_columns preprocessors authorized_models yaml_column_permitted_classes]
)
end

it 'resets ignorable_columns' do
expect { subject }.to change { klass.ignorable_columns }.to %w[id created_at updated_at]
end
Expand All @@ -157,6 +168,16 @@
expect { subject }.to change { klass.authorized_models }.to :any
end

if ActiveRecord.respond_to?(:yaml_column_permitted_classes)
it 'resets yaml_column_permitted_classes' do
expect { subject }.to change { klass.yaml_column_permitted_classes }.to match_array(ActiveRecord.yaml_column_permitted_classes + [Date, Time, DateTime])
end
else
it 'resets yaml_column_permitted_classes' do
expect { subject }.to change { klass.yaml_column_permitted_classes }.to match_array([Date, Time, DateTime])
end
end

it 'resets other options' do
expect { subject }.to change { klass.instance_variable_get(:@_config).a_random_option }.to nil
end
Expand Down
Loading

0 comments on commit af96c2d

Please sign in to comment.