active_record_anonymizer 0.1.0

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