Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/yaml premitted classes #5

Merged
merged 5 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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