active_record_anonymizer 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2595896543324c7c02e7876dc7094a6cef95453a782183426ce433e846e1e395
4
+ data.tar.gz: 01ddfc4df3b6a1458cb692e256951f96e3bd8995d098b5b263bc6671ac465ad7
5
+ SHA512:
6
+ metadata.gz: 99e3e06eeac716238b5690011905e5b9633b7bd88eb82693bed0c27c142f4614b6b8daeef4030719a6e79126a54ed26672e4a8741a01c0f5c18f22a3c5c11b60
7
+ data.tar.gz: '09b7bc01508b9c320cfa15f2857707bfc5556df6dd2af696143eeeb29790a11b7235a6137a58d34dff76151bb4b1b9feb606a8d8029119c9b449149641ec6776'
data/.rubocop.yml ADDED
@@ -0,0 +1,76 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+ NewCops: disable
4
+ SuggestExtensions: false
5
+ Exclude:
6
+ - "test/dummy/db/**/*"
7
+ - "test/dummy/bin/*"
8
+ - "test/dummy/config/**/*"
9
+
10
+ Style/HashSyntax:
11
+ EnforcedStyle: ruby19_no_mixed_keys
12
+ EnforcedShorthandSyntax: never
13
+
14
+ Style/GuardClause:
15
+ Enabled: false
16
+
17
+ Style/ClassAndModuleChildren:
18
+ Exclude:
19
+ - "test/**/*"
20
+
21
+ # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
22
+ # SupportedStyles: assign_to_condition, assign_inside_condition
23
+ Style/ConditionalAssignment:
24
+ Enabled: false
25
+
26
+ # Configuration parameters: AllowedConstants.
27
+ Style/Documentation:
28
+ Enabled: false
29
+
30
+ Style/IfUnlessModifier:
31
+ Enabled: false
32
+
33
+ # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
34
+ # SupportedStyles: single_quotes, double_quotes
35
+ Style/StringLiterals:
36
+ EnforcedStyle: double_quotes
37
+ ConsistentQuotesInMultiline: true
38
+
39
+ # Configuration parameters: EnforcedStyle.
40
+ # SupportedStyles: both, prefix, postfix
41
+ Style/NegatedIf:
42
+ Enabled: false
43
+
44
+ Style/RedundantBegin:
45
+ Enabled: false
46
+
47
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
48
+ # URISchemes: http, https
49
+ Layout/LineLength:
50
+ Max: 150
51
+
52
+ Metrics/AbcSize:
53
+ Max: 30
54
+
55
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
56
+ # AllowedMethods: refine
57
+ Metrics/BlockLength:
58
+ CountComments: false
59
+ Max: 30
60
+
61
+ # Configuration parameters: CountComments, CountAsOne.
62
+ Metrics/ClassLength:
63
+ Exclude:
64
+ - "test/**/*"
65
+
66
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
67
+ Metrics/CyclomaticComplexity:
68
+ Max: 17
69
+
70
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
71
+ Metrics/MethodLength:
72
+ Max: 30
73
+
74
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
75
+ Metrics/PerceivedComplexity:
76
+ Max: 15
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in active_record_anonymizer.gemspec
6
+ gemspec
7
+
8
+ gem "minitest", "~> 5.0"
9
+ gem "pry", "~> 0.14.2"
10
+ gem "rake", "~> 13.0"
11
+ gem "rubocop", "~> 1.21"
12
+
13
+ gem "mocha", "~> 2.1"
14
+ gem "mysql2", "~> 0.5"
15
+ gem "pg", "~> 1.2"
16
+ gem "rails", ">= 6.0.0"
17
+ gem "sqlite3", "~> 1.4"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Keshav Biswa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # ActiveRecordAnonymizer
2
+
3
+ Anonymize your ActiveRecord models with ease :sunglasses:
4
+
5
+ `ActiveRecordAnonymizer` uses `faker` to anonymize your ActiveRecord model's attributes without the need to write custom anonymization logic for each model.
6
+
7
+ Using `ActiveRecordAnonymizer`, you can:
8
+ - Anonymize specific attributes of your model (uses `faker` under the hood)
9
+ - Provide custom anonymization logic for specific attributes
10
+ - Provide custom anonymized columns for each attributes
11
+ - Encrypt anonymized data using `ActiveRecord::Encryption` (Requires Rails 7+)
12
+ - Environment dependent, so you can decide whether you want to view original data in development and anonymized data in production
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'active_record_anonymizer'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ $ bundle install
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install active_record_anonymizer
29
+
30
+ Install the gem using the following command:
31
+
32
+ $ bin/rails generate active_record_anonymizer:install
33
+
34
+ You must have anonymized columns in your Model to store the anonymized data.
35
+ You can use the following migration generator to add anonymized columns to your existing table:
36
+
37
+ $ bin/rails generate anonymize User first_name last_name
38
+ This will generate a migration file similar to the following:
39
+
40
+ ```ruby
41
+ class AddAnonymizedColumnsToUser < ActiveRecord::Migration[6.0]
42
+ def change
43
+ add_column :users, :anonymized_first_name, :string
44
+ add_column :users, :anonymized_last_name, :string
45
+ end
46
+ end
47
+ ```
48
+
49
+ Add the following line to your model to enable anonymization:
50
+
51
+ ```ruby
52
+ class User < ApplicationRecord
53
+ # There are other options available, please refer to the Usage section
54
+ anonymize :first_name, :last_name
55
+ end
56
+ ```
57
+ To populate the anonymized columns, run the following command:
58
+
59
+ $ bin/rails anonymize:populate CLASS=User
60
+
61
+ The `CLASS` argument is optional, if not provided, it will anonymize all the models with anonymized columns.
62
+
63
+ ## Usage
64
+
65
+ Attributes can be anonymized using the `anonymize` method. The `anonymize` method takes the following options:
66
+
67
+ - `:column_name` - The name of the column to store the anonymized data. ("anonymized_#{column_name}" by default)
68
+ - `:with` - The custom logic to anonymize the attribute. (Optional, uses `faker` by default)
69
+ - `:encrypt` - Encrypt the anonymized data using `ActiveRecord::Encryption` (Requires Rails 7+)
70
+
71
+ ```ruby
72
+ class User < ApplicationRecord
73
+ anonymize :first_name, :last_name
74
+ anonymize :email, with: ->(email) { Faker::Internet.email }
75
+ anonymize :age, with: ->(age) { age + 5 }
76
+ anonymize :phone, column_name: :fake_phone_number, with: ->(phone) { phone.gsub(/\d/, 'X') }
77
+ anonymize :address, encrypt: true
78
+ end
79
+ ```
80
+
81
+ ## Configuration
82
+
83
+ You can configure the gem using the following options:
84
+
85
+ - `:environments` - The environments in which the anonymized data should be used. (Defaults to `[:staging]`)
86
+ - `:skip_update` - Skip updating the anonymized data when the record is updated. This ensures your anonymized data remains the same even if it's updated. (Defaults to `false`)
87
+ - `:alias_original_columns` - Alias the original columns to the anonymized columns. You can still access the original value of the attribute using the alias `original_#{attribute_name}`(Defaults to `false`)
88
+ - `:alias_column_name` - The name of the alias column. (Defaults to `original_#{column_name}`)
89
+
90
+ ```ruby
91
+ ActiveRecordAnonymizer.configure do |config|
92
+ config.environments = [:staging, :production] # The environments in which the anonymized data should be used
93
+ config.skip_update = true # Skip updating the anonymized data when the record is updated
94
+ config.alias_original_columns = true # Alias the original columns to the anonymized columns
95
+ config.alias_column_name = "original" # The original column will be aliased to "original_#{column_name}
96
+ end
97
+ ```
98
+
99
+ ## Development
100
+
101
+ After checking out the repo, run `bin/setup` to install dependencies.
102
+ Then, run `rake test` to run the tests.
103
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
104
+
105
+ ## Contributing
106
+
107
+ Bug reports are welcome on GitHub at https://github.com/keshavbiswa/active_record_anonymizer/issues.
108
+
109
+ - Fork the Repository: Start by forking this [repo](https://github.com/keshavbiswa/active_record_anonymizer.git) on GitHub.
110
+
111
+ - Set Up Your Local Environment: Navigate into the project directory and run the setup script to install dependencies:
112
+
113
+ ```shell
114
+ $ cd active_record_anonymizer
115
+ $ bin/setup
116
+ ```
117
+ - Create a New Branch: Before making any changes, create a new branch to keep your work organized:
118
+
119
+ ```shell
120
+ $ git checkout -b my-new-feature
121
+ ```
122
+
123
+ - Make Your Changes:
124
+ - Implement your changes or fixes in your local repository.
125
+ - Be sure to keep your changes as focused as possible.
126
+ - If you're working on multiple unrelated improvements, consider making separate branches and pull requests for each.
127
+
128
+ - Write Tests: If you're adding a new feature or fixing a bug, please add or update the corresponding tests.
129
+
130
+ - Run Tests: Before submitting your changes, run the test suite to ensure everything is working correctly:
131
+ ```shell
132
+ $ bin/rake test
133
+ ```
134
+
135
+ - Update Documentation: If your changes involve user-facing features or APIs, update the README or other relevant documentation accordingly.
136
+
137
+ - Submit a Pull Request: Go to the original `ActiveRecordAnonymizer` repository on GitHub, and you'll see a prompt to submit a pull request from your new branch.
138
+
139
+
140
+ Bug reports and pull requests are welcome on GitHub at https://github.com/keshavbiswa/active_record_anonymizer.
141
+
142
+ ## License
143
+
144
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordAnonymizer
4
+ class Anonymizer
5
+ attr_reader :model, :attributes, :with, :column_name
6
+
7
+ def initialize(model, attributes, with: nil, column_name: nil)
8
+ @model = model
9
+ @attributes = attributes
10
+ @with = with
11
+ @column_name = column_name
12
+ end
13
+
14
+ # TODO: Extract this logic in a seperate Validation module/class
15
+ def validate
16
+ check_for_invalid_arguments(attributes, with, column_name)
17
+ check_for_missing_anonymized_columns(attributes)
18
+ end
19
+
20
+ def anonymize_attributes
21
+ attributes.each do |attribute|
22
+ anonymized_column = anonymized_column_name(attribute)
23
+
24
+ # I don't like that we're manipulating the class attribute here
25
+ # This breaks the SRP for this method
26
+ # TODO:- Will need to revisit how we set the class attribute later
27
+ model.anonymized_attributes[attribute.to_sym] = { column: anonymized_column.to_sym, with: with }
28
+
29
+ define_alias_method(attribute, ActiveRecordAnonymizer.configuration.alias_column_name) if ActiveRecordAnonymizer.alias_enabled?
30
+ define_anonymize_method(attribute, anonymized_column)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def check_for_missing_anonymized_columns(attributes)
37
+ missing_columns = attributes.reject do |attribute|
38
+ model.columns_hash[anonymized_column_name(attribute)]
39
+ end
40
+
41
+ if missing_columns.any?
42
+ raise ColumnNotFoundError, <<~ERROR_MESSAGE.strip
43
+ Following columns do not have anonymized_columns: #{missing_columns.join(', ')}.
44
+ You can generate them by running
45
+ `rails g active_record_anonymizer:anonymize #{model.name} #{missing_columns.join(' ')}`
46
+ ERROR_MESSAGE
47
+ end
48
+ end
49
+
50
+ def check_for_invalid_arguments(attributes, with, column_name)
51
+ if attributes.size > 1 && (with || column_name)
52
+ raise InvalidArgumentsError, "with and column_names are not supported for multiple attributes. Try adding them seperately"
53
+ end
54
+ end
55
+
56
+ def anonymized_column_name(attribute)
57
+ column_name || generated_column_name(attribute)
58
+ end
59
+
60
+ def generated_column_name(attribute)
61
+ "anonymized_#{attribute}"
62
+ end
63
+
64
+ # This defines a method that returns the anonymized value of the attribute.
65
+ # It also creates an alias "original_#{attribute}" that returns the original value. (TODO)
66
+ # If column_name is provided, it will be used instead of "anonymized_#{attribute}"
67
+ def define_anonymize_method(attribute, anonymized_attr)
68
+ model.define_method(attribute) do
69
+ ActiveRecordAnonymizer.anonymization_enabled? ? self[anonymized_attr] : self[attribute]
70
+ end
71
+ end
72
+
73
+ def define_alias_method(attribute, alias_column_name)
74
+ model.define_method("#{alias_column_name}_#{attribute}") do
75
+ self[attribute]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordAnonymizer
4
+ class Configuration
5
+ attr_accessor :environments, :skip_update, :alias_original_columns, :alias_column_name
6
+
7
+ def initialize
8
+ @environments = %i[staging]
9
+ @skip_update = false
10
+ @alias_original_columns = false
11
+ @alias_column_name = "original"
12
+ end
13
+
14
+ # Reset all configuration options to defaults.
15
+ # Required for tests to maintain isolation between test cases.
16
+ def reset
17
+ initialize
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordAnonymizer
4
+ class Encryptor
5
+ attr_reader :attributes
6
+
7
+ def initialize(model, attributes)
8
+ @model = model
9
+ @attributes = attributes
10
+ end
11
+
12
+ def encrypt
13
+ if !rails_version_supported?
14
+ raise ActiveRecordAnonymizer::UnsupportedVersionError,
15
+ "ActiveRecordAnonymizer relies on Rails 7+ for encrypted columns."
16
+ end
17
+
18
+ @model.encrypts(*@attributes)
19
+ end
20
+
21
+ private
22
+
23
+ def rails_version_supported?
24
+ ActiveRecord::VERSION::MAJOR >= 7 && ActiveRecord::VERSION::MINOR >= 0
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module ActiveRecordAnonymizer
6
+ module Extensions
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def ensure_mutex_initialized
11
+ unless defined?(@setup_mutex)
12
+ @setup_mutex = Mutex.new
13
+ end
14
+ end
15
+
16
+ def anonymize(*attributes, with: nil, column_name: nil, encrypted: false)
17
+ ensure_mutex_initialized
18
+ Encryptor.new(self, attributes).encrypt if encrypted
19
+
20
+ ActiveRecordAnonymizer.register_model(self)
21
+
22
+ # These class variables required to generate anonymized values
23
+ # These are not thread safe!
24
+ cattr_accessor :anonymized_attributes, instance_accessor: false unless respond_to?(:anonymized_attributes)
25
+ self.anonymized_attributes ||= {}
26
+
27
+ anonymizer = Anonymizer.new(self, attributes, with: with, column_name: column_name)
28
+ anonymizer.validate
29
+ anonymizer.anonymize_attributes
30
+
31
+ # I'm ensuring that the before_save callback is only added once
32
+ # Models can call anonymize method multiple times per column
33
+ @setup_mutex.synchronize do
34
+ unless @anonymizer_setup_done
35
+ before_save :anonymize_columns, if: :anonymization_enabled?
36
+ @anonymizer_setup_done = true
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def anonymization_enabled?
43
+ ActiveRecordAnonymizer.anonymization_enabled?
44
+ end
45
+
46
+ def anonymize_columns
47
+ if new_record?
48
+ # For new records, apply anonymization to all attributes
49
+ anonymize_all_attributes
50
+ else
51
+ # For existing records, only apply to attributes that have changed
52
+ return if ActiveRecordAnonymizer.configuration.skip_update
53
+
54
+ anonymize_changed_attributes
55
+ end
56
+ end
57
+
58
+ def anonymize_all_attributes
59
+ self.class.anonymized_attributes.each_value do |settings|
60
+ generate_and_write_fake_value(settings[:column], settings[:with])
61
+ end
62
+ end
63
+
64
+ def anonymize_changed_attributes
65
+ changes = self.changes.keys.map(&:to_sym)
66
+ changed_attributes = changes & self.class.anonymized_attributes.keys
67
+
68
+ changed_attributes.each do |attribute|
69
+ settings = self.class.anonymized_attributes[attribute]
70
+ generate_and_write_fake_value(settings[:column], settings[:with])
71
+ end
72
+ end
73
+
74
+ def generate_and_write_fake_value(anonymized_attr, with_strategy = nil)
75
+ fake_value = case with_strategy
76
+ when Proc
77
+ with_strategy.call(self)
78
+ when Symbol
79
+ send(with_strategy)
80
+ else
81
+ FakeValue.new(anonymized_attr, self.class.columns_hash[anonymized_attr.to_s]).generate_fake_value
82
+ end
83
+ write_attribute(anonymized_attr, fake_value)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordAnonymizer
4
+ class FakeValue
5
+ def initialize(name, column)
6
+ @name = name
7
+ @column = column
8
+ end
9
+
10
+ def generate_fake_value
11
+ case @column.type
12
+ when :string, :text, :citext then Faker::Lorem.word
13
+ when :uuid then SecureRandom.uuid
14
+ when :integer, :bigint, :smallint then Faker::Number.number(digits: 5)
15
+ when :decimal, :float, :real then Faker::Number.decimal(l_digits: 2, r_digits: 2)
16
+ when :datetime, :timestamp, :timestamptz then Faker::Time.between(from: DateTime.now - 1, to: DateTime.now)
17
+ when :date then Faker::Date.between(from: Date.today - 2, to: Date.today)
18
+ when :time, :timetz then Faker::Time.forward(days: 23, period: :morning)
19
+ when :boolean then Faker::Boolean.boolean
20
+ when :json, :jsonb then generate_json
21
+ when :inet then Faker::Internet.ip_v4_address
22
+ when :cidr, :macaddr then Faker::Internet.mac_address
23
+ when :bytea then Faker::Internet.password
24
+ when :bit, :bit_varying then %w[0 1].sample
25
+ when :money then generate_money
26
+ when :hstore then generate_json
27
+ when :year then rand(1901..2155)
28
+ else raise UnknownColumnTypeError, "Unknown column type: #{@column.type}"
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def generate_json
35
+ { "value" => { "key1" => Faker::Lorem.word, "key2" => Faker::Number.number(digits: 2) } }
36
+ end
37
+
38
+ def generate_money
39
+ Faker::Commerce.price.to_s
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordAnonymizer
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load "tasks/active_record_anonymizer.rake"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordAnonymizer
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "zeitwerk"
5
+ require "faker"
6
+
7
+ module ActiveRecordAnonymizer
8
+ @mutex = Mutex.new
9
+
10
+ @loader = Zeitwerk::Loader.for_gem
11
+ @loader.ignore("#{__dir__}/generators")
12
+ @loader.setup
13
+
14
+ class Error < StandardError; end
15
+ class ColumnNotFoundError < StandardError; end
16
+ class InvalidArgumentsError < StandardError; end
17
+ class UnknownColumnTypeError < StandardError; end
18
+ class UnsupportedVersionError < StandardError; end
19
+
20
+ class << self
21
+ attr_reader :loader
22
+
23
+ def register_model(model)
24
+ @mutex.synchronize do
25
+ @models ||= []
26
+ @models << model unless @models.include?(model)
27
+ end
28
+ end
29
+
30
+ def models
31
+ @mutex.synchronize do
32
+ @models ||= []
33
+ end
34
+ end
35
+
36
+ def configure
37
+ @mutex.synchronize do
38
+ yield configuration if block_given?
39
+ end
40
+ end
41
+
42
+ def configuration
43
+ Thread.current[:active_record_anonymizer_configuration] ||= Configuration.new
44
+ end
45
+
46
+ def eager_load!
47
+ loader.eager_load
48
+ end
49
+
50
+ def anonymization_enabled?
51
+ @mutex.synchronize do
52
+ configuration.environments.include?(Rails.env.to_sym)
53
+ end
54
+ end
55
+
56
+ def alias_enabled?
57
+ @mutex.synchronize do
58
+ configuration.alias_original_columns
59
+ end
60
+ end
61
+
62
+ def load_model(klass_name)
63
+ model = klass_name.safe_constantize
64
+ raise Error, "Could not find class: #{klass_name}" unless model
65
+
66
+ unless models.include?(model)
67
+ raise Error, "#{klass_name} is not an anonymized model"
68
+ end
69
+
70
+ model
71
+ end
72
+ end
73
+ end
74
+
75
+ ActiveSupport.on_load(:active_record) do
76
+ include ActiveRecordAnonymizer::Extensions
77
+ end
78
+
79
+ ActiveRecordAnonymizer.eager_load!
@@ -0,0 +1,10 @@
1
+ Description:
2
+ This generator creates a migration to anonymize a model's fields.
3
+ If the generator is used to generate migration for a model with field "name",
4
+ it will create a migration to create a new field "anonymized_name"
5
+
6
+ Example:
7
+ bin/rails generate anonymize Model field1 field2
8
+
9
+ This will create:
10
+ db/migrate/20160101000000_anonymize_model.rb
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module ActiveRecordAnonymizer
7
+ class AnonymizeGenerator < ActiveRecord::Generators::Base
8
+ include Rails::Generators::Migration
9
+
10
+ class InvalidArguments < Thor::Error; end
11
+
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ argument :attributes, type: :array, default: [], banner: "attribute attribute"
15
+
16
+ def generate_migration
17
+ validate_arguments
18
+ migration_template "migration.rb.erb", "db/migrate/anonymize_#{table_name}.rb"
19
+ output_instructions
20
+ end
21
+
22
+ private
23
+
24
+ def output_instructions
25
+ log "\n\n"
26
+ log "Add the following to your model file:"
27
+ log "\n"
28
+ log anonymize_method_string
29
+ log "\n"
30
+ end
31
+
32
+ def anonymize_method_string
33
+ " anonymize :#{attributes.map(&:name).join(', :')}\n"
34
+ end
35
+
36
+ def validate_arguments
37
+ raise InvalidArguments, "Attributes are needed for the migration" if attributes.empty?
38
+ end
39
+
40
+ def migration_version
41
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if Rails.version.start_with? "5"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module ActiveRecordAnonymizer
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ desc "Creates an anonymizer initializer file"
10
+ def copy_initializer
11
+ template "anonymizer.rb", "config/initializers/anonymizer.rb"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecordAnonymizer.configure do |config|
4
+ # Configure the environments in which anonymization is allowed.
5
+ config.environments = %i[staging]
6
+
7
+ # Uncomment the following line to skip updating anonymized_columns when updating the original columns.
8
+ # config.skip_update = true
9
+
10
+ # Uncomment the following line to alias the original columns.
11
+ # config.alias_original_columns = true
12
+
13
+ # This will only work if config.alias_original_columns is set to true.
14
+ # Change the alias column name. (original by default)
15
+ # Model.original_column_name will provide the original value of the column.
16
+ config.alias_column_name = "original"
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
4
+ def self.up
5
+ <% attributes.each do |column| -%>
6
+ add_column :<%= table_name %>, :<%= "anonymized_#{column.name}" %>, :<%= column.type %>, index: <%= column.has_index? %>, unique: <%= column.has_uniq_index? %>
7
+ <% end -%>
8
+ end
9
+
10
+ def self.down
11
+ <%- attributes.each do |column| -%>
12
+ remove_column :<%= table_name %>, :<%= "anonymized_#{column.name}" %>
13
+ <% end -%>
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_anonymizer"
4
+
5
+ namespace :anonymizer do
6
+ desc "populate anonymize columns (specify CLASS)"
7
+ task populate: :environment do
8
+ klass_name = ENV["CLASS"]
9
+ abort "USAGE: rake anonymize:populate CLASS=User" unless klass_name
10
+
11
+ model =
12
+ begin
13
+ ActiveRecordAnonymizer.load_model(klass_name)
14
+ rescue ActiveRecordAnonymizer::Error => e
15
+ abort e.message
16
+ end
17
+
18
+ puts "Anonymize columns for #{klass_name}..."
19
+ model.each(&:save!)
20
+ puts "Anonymize columns for #{klass_name} done!"
21
+ end
22
+
23
+ namespace :populate do
24
+ desc "populate anonymize columns for all models"
25
+ task all: :environment do
26
+ ActiveRecordAnonymizer.models.each do |model|
27
+ puts "Anonymizing #{model.name}..."
28
+ model.find_each(&:save!)
29
+ end
30
+ puts "Anonymize columns for all models done!"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveRecordAnonymizer
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record_anonymizer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Keshav Biswa
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: faker
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.9'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.9'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.4'
69
+ description: " A Rails gem to anonymize ActiveRecord attributes using Faker and
70
+ other strategies.\n"
71
+ email:
72
+ - keshavbiswa21@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".rubocop.yml"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - lib/active_record_anonymizer.rb
83
+ - lib/active_record_anonymizer/anonymizer.rb
84
+ - lib/active_record_anonymizer/configuration.rb
85
+ - lib/active_record_anonymizer/encryptor.rb
86
+ - lib/active_record_anonymizer/extensions.rb
87
+ - lib/active_record_anonymizer/fake_value.rb
88
+ - lib/active_record_anonymizer/railtie.rb
89
+ - lib/active_record_anonymizer/version.rb
90
+ - lib/generators/active_record_anonymizer/USAGE
91
+ - lib/generators/active_record_anonymizer/anonymize_generator.rb
92
+ - lib/generators/active_record_anonymizer/install_generator.rb
93
+ - lib/generators/active_record_anonymizer/templates/anonymizer.rb
94
+ - lib/generators/active_record_anonymizer/templates/migration.rb.erb
95
+ - lib/tasks/active_record_anonymizer.rake
96
+ - sig/active_record_anonymizer.rbs
97
+ homepage: https://github.com/keshavbiswa/active_record_anonymizer
98
+ licenses:
99
+ - MIT
100
+ metadata:
101
+ homepage_uri: https://github.com/keshavbiswa/active_record_anonymizer
102
+ source_code_uri: https://github.com/keshavbiswa/active_record_anonymizer
103
+ changelog_uri: https://github.com/keshavbiswa/active_record_anonymizer
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 2.7.7
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubygems_version: 3.4.10
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Anonymize your ActiveRecord attributes with ease.
123
+ test_files: []