anonymizable 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f168893c5332b239f219373fc9534013f56717d4
4
+ data.tar.gz: 2ce8b584bb2c0ff8f05279d49d5482a78a1d3d04
5
+ SHA512:
6
+ metadata.gz: 76ff17fe8175c8842031120ccc8269694ed1e9ec316ed43f6373176ff50b457d268b6d74ae571654e8df67d9954549a947aa980c3814e416133e8c2833a99bbe
7
+ data.tar.gz: a386282d9d3a9807fd3cab27b2ae5a5d981095545dda91fa006498dd08a7d1aa2601beb6179869578b8f0254980a71d404ae2cb414abe0f5ac0df8cc6b746a59
@@ -0,0 +1,13 @@
1
+ *.gem
2
+ *.swp
3
+ .bundle
4
+ .config
5
+ .rvmrc
6
+ .DS_Store
7
+ Gemfile.lock
8
+ spec/debug.log
9
+ spec/*.db
10
+ coverage
11
+ pkg
12
+ rdoc
13
+ TODO
data/.pryrc ADDED
@@ -0,0 +1,32 @@
1
+ require 'yaml'
2
+ require 'anonymizable'
3
+
4
+ def user
5
+ with_retries do
6
+ @user ||= User.create! first_name: "Created", last_name: "User", email: "created.user@foobar.com", password: "foobar", role: role
7
+ end
8
+ end
9
+
10
+ def role
11
+ with_retries do
12
+ @role ||= Role.create! name: "user"
13
+ end
14
+ end
15
+
16
+ def with_retries(attempts=1, &block)
17
+ retries = 1
18
+ begin
19
+ yield
20
+ rescue NameError
21
+ load_models
22
+ retry unless retries > attempts
23
+ retries += 1
24
+ end
25
+ end
26
+
27
+ def load_models
28
+ config = YAML::load(IO.read(File.join(Dir.pwd, "spec/database.yml")))
29
+ ActiveRecord::Base.establish_connection(config['sqlite3'])
30
+ load(File.join(Dir.pwd, "spec/schema.rb"))
31
+ Dir.glob(File.join(Dir.pwd, "/spec/models/*.rb")) { |path| require path }
32
+ end
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,203 @@
1
+ # Anonymizable
2
+
3
+ Anonymizable adds the ability to anonymize or delete data in your ActiveRecord models. A good use case for this is when you want to remove user accounts but for statistical or data integrity reasons you want to keep the actual records in your database.
4
+
5
+ ## Supported Ruby/Rails Versions
6
+
7
+ Anonymizer supports all 2.x versions of Ruby and all Rails versions between 3.2.x and 4.x.
8
+
9
+ ## Installation
10
+
11
+ ```ruby
12
+ gem 'anonymizable'
13
+ ```
14
+ and run `bundle install` from your shell.
15
+
16
+ ## Usage
17
+
18
+ ### Nullification
19
+
20
+ ```ruby
21
+ class User < ActiveRecord::Base
22
+
23
+ anonymizable do
24
+ attributes :first_name, :last_name
25
+ end
26
+
27
+ end
28
+ ```
29
+
30
+ This adds an instance method ```User#anonymize!``` which in this case just nullifies the ```first_name``` and```last_name``` database columns.
31
+
32
+ The ```anonymize!``` method is defined as private by default, but you can make it public by passing the ```public``` option:
33
+
34
+ ```ruby
35
+ anonymizable public: true do
36
+ attributes :first_name, :last_name
37
+ end
38
+ ```
39
+
40
+ ### Anonymization
41
+
42
+ By passing to ```attributes``` a hash as the last argument, you can anonymize columns using a Proc or instance method.
43
+
44
+ ```ruby
45
+ anonymizable public: true do
46
+
47
+ attributes :first_name, :last_name,
48
+ email: Proc.new { |u| "anonymized.user.#{u.id}@foobar.com" },
49
+ password: :random_password
50
+ end
51
+ ```
52
+
53
+ In this example, the ```email``` column will be anonymized by calling the block and passing the ```User``` object. The ```password``` column will be anonymized using the return value of ```User#random_password```, which can be defined as either a public or private instance method. The user object is not passed as an argument in this case, but can be accessed by ```self```.
54
+
55
+ ### Associations
56
+
57
+ ActiveRecord associations can either be anonymized, destroyed, or deleted.
58
+
59
+ ```ruby
60
+ anonymizable public: true do
61
+
62
+ attributes :first_name, :last_name,
63
+ email: Proc.new { |u| "anonymized.user.#{u.id}@foobar.com" },
64
+ password: :random_password
65
+
66
+ associations do
67
+ anonymize :posts, :comments
68
+ delete :avatar, :likes
69
+ destroy :images
70
+ end
71
+
72
+ end
73
+ ```
74
+
75
+ In the example above, the ```anonymize!``` method will be called on each ```Post``` and ```Comment``` association. As such, anonymization must be defined on both of these classes:
76
+
77
+ ```ruby
78
+ class Post < ActiveRecord::Base
79
+
80
+ anonymizable :user_id
81
+
82
+ end
83
+ ```
84
+
85
+ ```ruby
86
+ class Comment < ActiveRecord::Base
87
+
88
+ anonymizable :user_id
89
+
90
+ end
91
+ ```
92
+
93
+ In this case, the ```user_id``` column is nullified on any of the user's posts or comments.
94
+
95
+
96
+ **All operations on columns and attributes are performed in a database transaction which will rollback all changes if an error occurs during anonymization.**
97
+
98
+
99
+ ### Guards
100
+
101
+ You can declare a Proc or method to use as a guard against anonymization. If the Proc or method returns ```ruby false``` or ```ruby nil```, anonymization will short circuit.
102
+
103
+ ```ruby
104
+ anonymizable public: true do
105
+ only_if :can_anonymize?
106
+
107
+ attributes :first_name, :last_name,
108
+ email: Proc.new { |u| "anonymized.user.#{u.id}@foobar.com" },
109
+ password: :random_password
110
+
111
+ associations do
112
+ anonymize :posts, :comments
113
+ delete :avatar, :likes
114
+ destroy :images
115
+ end
116
+
117
+ end
118
+
119
+ def can_anonymize?
120
+ !admin?
121
+ end
122
+ ```
123
+
124
+ ### Preventing Deletion
125
+
126
+ You can prohibit the deletion of objects on which anonymizable is configured by passing the ```raise_on_delete``` option:
127
+
128
+ ```ruby
129
+ anonymizable raise_on_delete: true do
130
+
131
+ attributes :first_name, :last_name,
132
+ email: Proc.new { |u| "anonymized.user.#{u.id}@foobar.com" },
133
+ password: :random_password
134
+
135
+ associations do
136
+ anonymize :posts, :comments
137
+ delete :avatar, :likes
138
+ destroy :images
139
+ end
140
+
141
+ end
142
+ ```
143
+
144
+ ```ruby
145
+ > user.delete
146
+ Anonymizable::DeleteProhibitedError: destroy is prohibited on #<User:0x007f8d125e4948>
147
+ ```
148
+
149
+ ```ruby
150
+ > user.destroy
151
+ Anonymizable::DestroyProhibitedError: destroy is prohibited on #<User:0x007f8d125e4948>
152
+ ```
153
+
154
+ ### Callbacks
155
+
156
+ You can declare callbacks that run after anonymization is complete.
157
+
158
+ ```ruby
159
+ anonymizable public: true do
160
+
161
+ attributes :first_name, :last_name,
162
+ email: Proc.new { |u| "anonymized.user.#{u.id}@foobar.com" },
163
+ password: :random_password
164
+
165
+ associations do
166
+ anonymize :posts, :comments
167
+ delete :avatar, :likes
168
+ destroy :images
169
+ end
170
+
171
+ after :email_admin, Proc.new { |original_attrs| log("Attributes changed: #{original_attrs}") }
172
+ end
173
+ ```
174
+
175
+ Each method or Proc is passed the value of the object's pre-anonymization attributes as a hash. You would define a method on ```User`` that receives the attribute hash:
176
+
177
+ ```ruby
178
+ def email_admin(original_attributes)
179
+ AdminMailer.user_anonymized(original_attributes["email"])
180
+ end
181
+ ```
182
+
183
+ It is worth noting that these callbacks are run after the database transaction commits, so an error in a callback does not trigger a database rollback.
184
+
185
+ ### Short Syntax
186
+
187
+ As intimated in the ```Post``` and ```Comment``` examples above, you can call ```anonymize``` without a block, but rather just with an array of columns to nullify and/or anonymization hash. In this case the ```Model#anonymize!``` will be private.
188
+
189
+ ## Contributing
190
+
191
+ 1. Fork it
192
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
193
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
194
+ 4. Push to the branch (`git push origin my-new-feature`)
195
+ 5. Create new Pull Request
196
+
197
+ ## License
198
+
199
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
200
+
201
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
202
+
203
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.unshift File.expand_path("../lib", __FILE__)
3
+ require "anonymizable/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "anonymizable"
7
+ s.description = "Delete data without deleting it"
8
+ s.summary = "Anonymize columns in ActiveRecord models"
9
+ s.homepage = "https://github.com/47primes/anonymizable"
10
+ s.authors = ["Mike Bradford"]
11
+ s.email = ["mbradford@47primes.com"]
12
+ s.version = Anonymizable::VERSION
13
+ s.platform = Gem::Platform::RUBY
14
+ s.license = 'MIT'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.require_paths = ["lib"]
18
+ s.add_dependency "activerecord", ">= 3.2", "< 5.0"
19
+
20
+ s.add_development_dependency "rspec", "~> 3.2"
21
+ s.add_development_dependency "sqlite3", "~> 1.3"
22
+ s.add_development_dependency "database_cleaner", "~> 1.0"
23
+ s.add_development_dependency "factory_girl", "~> 4.5"
24
+ s.add_development_dependency "pry", "~> 0.10"
25
+ end
@@ -0,0 +1,150 @@
1
+ require "active_record"
2
+ require "anonymizable/configuration"
3
+
4
+ module Anonymizable
5
+ AnonymizeError = Class.new(StandardError)
6
+ DeleteProhibitedError = Class.new(StandardError)
7
+ DestroyProhibitedError = Class.new(StandardError)
8
+
9
+ def self.extended(klass)
10
+
11
+ klass.class_eval do
12
+
13
+ class << self
14
+ attr_reader :anonymization_config
15
+
16
+ def anonymizable(*attrs, &block)
17
+ @anonymization_config ||= Configuration.new(self)
18
+ if block
19
+ options = attrs.extract_options!
20
+ @anonymization_config.send(:public) if options[:public] == true
21
+ @anonymization_config.send(:raise_on_delete) if options[:raise_on_delete] == true
22
+ @anonymization_config.instance_eval(&block)
23
+ else
24
+ @anonymization_config.attributes(*attrs)
25
+ end
26
+
27
+ define_method(:anonymize!) do
28
+ return false unless _can_anonymize?
29
+
30
+ original_attributes = attributes.dup
31
+ transaction do
32
+ _anonymize_columns
33
+ _anonymize_associations
34
+ _delete_associations
35
+ _destroy_associations
36
+ end
37
+ _perform_post_anonymization_callbacks(original_attributes)
38
+ true
39
+ end
40
+
41
+ unless @anonymization_config.public?
42
+ self.send(:private, :anonymize!)
43
+ end
44
+
45
+ if @anonymization_config.raise_on_delete?
46
+ define_method(:delete) do
47
+ raise DeleteProhibitedError.new("delete is prohibited on #{self}")
48
+ end
49
+
50
+ define_method(:destroy) do
51
+ raise DestroyProhibitedError.new("destroy is prohibited on #{self}")
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def _can_anonymize?
60
+ if self.class.anonymization_config.guard
61
+ if self.class.anonymization_config.guard.respond_to?(:call)
62
+ return self.class.anonymization_config.guard.call(self)
63
+ else
64
+ return self.send self.class.anonymization_config.guard
65
+ end
66
+ end
67
+
68
+ true
69
+ end
70
+
71
+ def _anonymize_columns
72
+ nullify_hash = self.class.anonymization_config.attrs_to_nullify.inject({}) {|memo, attr| memo[attr] = nil; memo}
73
+ anonymize_hash = self.class.anonymization_config.attrs_to_anonymize.inject({}) do |memo, array|
74
+ attr, proc = array
75
+ if proc.respond_to?(:call)
76
+ memo[attr] = proc.call(self)
77
+ else
78
+ memo[attr] = self.send(proc)
79
+ end
80
+ memo
81
+ end
82
+
83
+ update_hash = nullify_hash.merge anonymize_hash
84
+
85
+ self.class.where(id: self.id).update_all(update_hash) unless update_hash.empty?
86
+ end
87
+
88
+ def _anonymize_by_call
89
+ return if self.class.anonymization_config.attrs_to_anonymize.empty?
90
+ update_hash = self.class.anonymization_config.attrs_to_anonymize.inject({}) do |memo, array|
91
+ attr, proc = array
92
+ if proc.respond_to?(:call)
93
+ memo[attr] = proc.call(self)
94
+ else
95
+ memo[attr] = self.send(proc)
96
+ end
97
+ memo
98
+ end
99
+ self.class.where(id: self.id).update_all(update_hash)
100
+ end
101
+
102
+ def _anonymize_associations
103
+ self.class.anonymization_config.associations_to_anonymize.each do |association|
104
+ if self.send(association).respond_to?(:each)
105
+ self.send(association).each {|a| a.send(:anonymize!) }
106
+ elsif self.send(association)
107
+ self.send(association).send(:anonymize!)
108
+ end
109
+ end
110
+ end
111
+
112
+ def _delete_associations
113
+ self.class.anonymization_config.associations_to_delete.each do |association|
114
+ if self.send(association).respond_to?(:each)
115
+ self.send(association).each {|r| r.delete}
116
+ elsif self.send(association)
117
+ self.send(association).delete
118
+ end
119
+ end
120
+ end
121
+
122
+ def _destroy_associations
123
+ self.class.anonymization_config.associations_to_destroy.each do |association|
124
+ if self.send(association).respond_to?(:each)
125
+ self.send(association).each {|r| r.destroy}
126
+ elsif self.send(association)
127
+ self.send(association).destroy
128
+ end
129
+ end
130
+ end
131
+
132
+ def _perform_post_anonymization_callbacks(original_attributes)
133
+ self.class.anonymization_config.post_anonymization_callbacks.each do |callback|
134
+ if callback.respond_to?(:call)
135
+ callback.call(original_attributes)
136
+ else
137
+ self.send(callback, original_attributes)
138
+ end
139
+ end
140
+ end
141
+
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ ActiveSupport.on_load(:active_record) do
149
+ ActiveRecord::Base.send(:extend, Anonymizable)
150
+ end
@@ -0,0 +1,100 @@
1
+ require 'set'
2
+
3
+ module Anonymizable
4
+ class ConfigurationError < ArgumentError; end
5
+
6
+ class Configuration
7
+
8
+ attr_reader :guard,
9
+ :attrs_to_nullify,
10
+ :attrs_to_anonymize,
11
+ :associations_to_anonymize,
12
+ :associations_to_delete,
13
+ :associations_to_destroy,
14
+ :post_anonymization_callbacks
15
+
16
+ def initialize(klass)
17
+ @klass = klass
18
+ @guard = nil
19
+ @attrs_to_nullify = Set.new
20
+ @attrs_to_anonymize = Hash.new
21
+ @associations_to_anonymize = Set.new
22
+ @associations_to_delete = Set.new
23
+ @associations_to_destroy = Set.new
24
+ @post_anonymization_callbacks = Set.new
25
+ @public = false
26
+ @raise_on_delete = false
27
+ end
28
+
29
+ def only_if(callback)
30
+ validate_callback(callback)
31
+
32
+ @guard = callback
33
+ end
34
+
35
+ def attributes(*attrs)
36
+ @attrs_to_anonymize.merge! attrs.extract_options!
37
+
38
+ attrs.each do |attr|
39
+ validate_attribute(attr)
40
+ end
41
+
42
+ @attrs_to_nullify += attrs
43
+ end
44
+
45
+ def associations(*associations, &block)
46
+ instance_eval(&block)
47
+ end
48
+
49
+ def after(*callbacks)
50
+ callbacks.each do |callback|
51
+ validate_callback(callback)
52
+ end
53
+
54
+ @post_anonymization_callbacks += callbacks
55
+ end
56
+
57
+ def public?
58
+ @public
59
+ end
60
+
61
+ def raise_on_delete?
62
+ @raise_on_delete
63
+ end
64
+
65
+ private
66
+
67
+ def public
68
+ @public = true
69
+ end
70
+
71
+ def raise_on_delete
72
+ @raise_on_delete = true
73
+ end
74
+
75
+ def anonymize(*associations)
76
+ @associations_to_anonymize += associations
77
+ end
78
+
79
+ def delete(*associations)
80
+ @associations_to_delete += associations
81
+ end
82
+
83
+ def destroy(*associations)
84
+ @associations_to_destroy += associations
85
+ end
86
+
87
+ def validate_callback(callback)
88
+ if !callback.respond_to?(:call) && !callback.is_a?(String) && !callback.is_a?(Symbol)
89
+ raise ConfigurationError.new("Expected #{callback} to respond to 'call' or be a string or symbol.")
90
+ end
91
+ end
92
+
93
+ def validate_attribute(attr)
94
+ if !@klass.attribute_names.include?(attr.to_s)
95
+ raise ConfigurationError.new("Nonexitent attribute #{attr} on #{@klass}.")
96
+ end
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ module Anonymizable
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,200 @@
1
+ require 'spec_helper'
2
+
3
+ describe Anonymizable do
4
+
5
+ describe "anonymizable" do
6
+
7
+ it "should define a private instance method named anonymize!" do
8
+ expect(Post.private_instance_methods(false)).to include(:anonymize!)
9
+ expect(Comment.private_instance_methods(false)).to include(:anonymize!)
10
+ expect(Like.private_instance_methods(false)).to include(:anonymize!)
11
+ end
12
+
13
+ it "should make anonymize! public if specified" do
14
+ expect(User.public_instance_methods(false)).to include(:anonymize!)
15
+ end
16
+
17
+ it "should raise an error if delete is called on the model when raise_on_delete option is set" do
18
+ user = FactoryGirl.create(:user)
19
+
20
+ expect { user.delete }.to raise_error(Anonymizable::DeleteProhibitedError, "delete is prohibited on User")
21
+ end
22
+
23
+ it "should raise an error if destroy is called on the model when raise_on_delete options is set" do
24
+ user = FactoryGirl.create(:user)
25
+
26
+ expect { user.destroy }.to raise_error(Anonymizable::DestroyProhibitedError, "destroy is prohibited on User")
27
+ end
28
+
29
+ end
30
+
31
+ describe "anonymize!" do
32
+
33
+ it "should be short-circuited by guard" do
34
+ admin = FactoryGirl.create(:admin)
35
+
36
+ expect(admin.anonymize!).to eq(false)
37
+ end
38
+
39
+ it "should nullify all attributes named" do
40
+ user = FactoryGirl.create(:user, first_name: "Joe", last_name: "user", profile: "I am a user")
41
+
42
+ expect(user.anonymize!).to eq(true)
43
+
44
+ user.reload
45
+
46
+ expect(user.first_name).to be_nil
47
+ expect(user.last_name).to be_nil
48
+ expect(user.profile).to be_nil
49
+ end
50
+
51
+ it "should roll back the transaction if anonymization fails" do
52
+ class Admin < User
53
+ anonymizable public: true do
54
+ attributes :role_id
55
+ end
56
+ end
57
+
58
+ user = Admin.create! first_name: "Admin",
59
+ last_name: "User",
60
+ profile: "I am an admin. Kinda.",
61
+ email: "admin@anonymizable.io",
62
+ role: FactoryGirl.create(:role),
63
+ password: "foobar"
64
+
65
+
66
+ expect { user.anonymize! }.to raise_error(ActiveRecord::StatementInvalid)
67
+
68
+ user.reload
69
+
70
+ expect(user.first_name).to eq("Admin")
71
+ expect(user.last_name).to eq("User")
72
+ expect(user.profile).to eq("I am an admin. Kinda.")
73
+ expect(user.email).to eq("admin@anonymizable.io")
74
+ expect(user.role.user?).to eq(true)
75
+ expect(user.password).to eq("foobar")
76
+ end
77
+
78
+ it "should anonymize attributes by proc" do
79
+ user = FactoryGirl.create(:user)
80
+ user.anonymize!
81
+
82
+ expect(user.reload.email).to eq("anonymized.user.#{user.id}@foobar.com")
83
+ end
84
+
85
+ it "should anonymize by method call" do
86
+ user = FactoryGirl.create(:user)
87
+ password = user.password
88
+ expect(user).to receive(:random_password).and_call_original
89
+
90
+ user.anonymize!
91
+
92
+ expect(user.reload.password == password).to eq(false)
93
+ end
94
+
95
+ it "should call anonymize! on specified associations" do
96
+ user = FactoryGirl.create(:user)
97
+ post = FactoryGirl.create(:post, user: user)
98
+ comment = FactoryGirl.create(:comment, post: post, user: user)
99
+
100
+ user.anonymize!
101
+
102
+ post.reload
103
+ comment.reload
104
+
105
+ expect(post.user_id).to be_nil
106
+ expect(comment.user_id).to be_nil
107
+ end
108
+
109
+ it "should destroy specified associations" do
110
+ user = FactoryGirl.create(:user)
111
+ 3.times { FactoryGirl.create(:image, user: user) }
112
+
113
+ user.anonymize!
114
+
115
+ expect(user.images(true).count).to eq(0)
116
+ end
117
+
118
+ it "should delete specified associations" do
119
+ avatar = FactoryGirl.create(:avatar)
120
+ user = avatar.user
121
+ 2.times { FactoryGirl.create(:like, user: user) }
122
+
123
+ user.anonymize!
124
+
125
+ expect(user.avatar(true)).to be_nil
126
+ expect(user.likes(true).count).to eq(0)
127
+ end
128
+
129
+ it "should fail if the specified association is not defined on the model" do
130
+ class Customer < User
131
+ anonymizable public: true do
132
+ associations do
133
+ anonymize :shopping_cart
134
+ end
135
+ end
136
+ end
137
+
138
+ user = Customer.create! email: "customer@anonymizable.io", password: "foobar",
139
+ role: FactoryGirl.create(:role)
140
+
141
+ expect { user.anonymize! }.to raise_error(NoMethodError)
142
+ end
143
+
144
+ it "should fail if after callback is not defined" do
145
+ class Person < User
146
+ anonymizable public: true do
147
+ after :foo
148
+ end
149
+ end
150
+
151
+ user = Person.create! email: "person@anonymizable.io", password: "foobar",
152
+ role: FactoryGirl.create(:role)
153
+
154
+ expect { user.anonymize! }.to raise_error(NoMethodError)
155
+ end
156
+
157
+ it "should fail if after callback method doesn't receive the correct number of arguments" do
158
+ class Person < User
159
+ anonymizable public: true do
160
+ after :foo
161
+ end
162
+
163
+ def foo
164
+ end
165
+ end
166
+
167
+ user = Person.create! email: "person@anonymizable.io", password: "foobar",
168
+ role: FactoryGirl.create(:role)
169
+
170
+ expect { user.anonymize! }.to raise_error(ArgumentError)
171
+ end
172
+
173
+ it "should not rollback the transaction if a failure occurs in an after callback" do
174
+ class Employee < User
175
+ anonymizable public: true do
176
+ attributes :first_name, :last_name, :profile,
177
+ email: Proc.new {|c| "anonymized.user.#{c.id}@anonymizable.io" },
178
+ password: :random_password
179
+
180
+ after Proc.new { raise "failure!" }
181
+ end
182
+ end
183
+
184
+ user = Employee.create! first_name: "John", last_name: "Doe", profile: "Hello world",
185
+ email: "employee@anonymizable.io", password: "foobar",
186
+ role: FactoryGirl.create(:role)
187
+
188
+ expect { user.anonymize! }.to raise_error(RuntimeError, "failure!")
189
+
190
+ user.reload
191
+
192
+ expect(user.first_name).to be_nil
193
+ expect(user.last_name).to be_nil
194
+ expect(user.profile).to be_nil
195
+ expect(user.email).to eq("anonymized.user.#{user.id}@anonymizable.io")
196
+ expect(user.password != "foobar").to eq(true)
197
+ end
198
+ end
199
+
200
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ describe Anonymizable::Configuration do
4
+
5
+ let(:config) { User.anonymization_config }
6
+
7
+ describe "only_if" do
8
+
9
+ it "can receive the name of the method to guard against anonymization" do
10
+ expect(config.guard).to eq :can_anonymize?
11
+ end
12
+
13
+ it "should set the proc to guard against anonymization" do
14
+ proc = Proc.new { true }
15
+
16
+ config.only_if proc
17
+
18
+ expect(config.guard).to eq proc
19
+ end
20
+
21
+ it "should fail if not passed a Proc, String, or Symbol" do
22
+ expect { config.only_if true }.to raise_error(Anonymizable::ConfigurationError, "Expected true to respond to 'call' or be a string or symbol.")
23
+ end
24
+
25
+ end
26
+
27
+ describe "attributes" do
28
+
29
+ it "should set database columns to nullify" do
30
+ expect(config.attrs_to_nullify).to contain_exactly :first_name, :last_name, :profile
31
+ end
32
+
33
+ it "should fail if any attribute passed is not defined in the model" do
34
+ expect { config.attributes :middle_name }.to raise_error(Anonymizable::ConfigurationError, "Nonexitent attribute middle_name on User.")
35
+ end
36
+
37
+ it "should set proc by which to anonymize model attribute" do
38
+ expect(config.attrs_to_anonymize[:email]).to be_a Proc
39
+ end
40
+
41
+ it "should set name of method by which to anonymize model attribute" do
42
+ expect(config.attrs_to_anonymize[:password]).to eq :random_password
43
+ end
44
+
45
+ end
46
+
47
+ describe "associations" do
48
+
49
+ describe "anonymize" do
50
+ it "should set names of associations to anonymize" do
51
+ expect(config.associations_to_anonymize).to contain_exactly :posts, :comments
52
+ end
53
+ end
54
+
55
+ describe "delete" do
56
+ it "should set names of associations to delete" do
57
+ expect(config.associations_to_delete).to contain_exactly :avatar, :likes
58
+ end
59
+ end
60
+
61
+ describe "destroy" do
62
+ it "should set names of associations to destroy" do
63
+ expect(config.associations_to_destroy).to contain_exactly :images
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ describe "after" do
70
+
71
+ it "should set name of methods to invoke after anonymization" do
72
+ expect(config.post_anonymization_callbacks.to_a).to eq [:email_user, :email_admin]
73
+ end
74
+
75
+ it "should set proc to call after anonymization" do
76
+ proc = Proc.new { "hello" }
77
+
78
+ config.after proc
79
+
80
+ expect(config.post_anonymization_callbacks.to_a).to eq [:email_user, :email_admin, proc]
81
+ end
82
+
83
+ end
84
+
85
+ describe "public" do
86
+
87
+ it "should set the public flag" do
88
+ expect(config.public?).to eq true
89
+ end
90
+
91
+ end
92
+
93
+ describe "raise_on_delete" do
94
+
95
+ it "should set the raise_on_delete flag" do
96
+ expect(config.raise_on_delete?).to eq true
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: spec/anonymize.sqlite3.db
@@ -0,0 +1,6 @@
1
+ FactoryGirl.define do
2
+ factory :comment do
3
+ post
4
+ text "comment"
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ FactoryGirl.define do
2
+ factory :image do
3
+ user
4
+ end
5
+
6
+ factory :avatar do
7
+ user
8
+ end
9
+
10
+ factory :thumbnail, class: Image do
11
+ image
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ FactoryGirl.define do
2
+ factory :like do
3
+ user
4
+ post
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ FactoryGirl.define do
2
+ factory :post do
3
+ title "title"
4
+ summary "summary"
5
+ content "content"
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ FactoryGirl.define do
2
+ factory :role, class: Role do
3
+ name "user"
4
+ end
5
+
6
+ factory :admin_role, class: Role do
7
+ name "admin"
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ FactoryGirl.define do
2
+ factory :user do
3
+ email { "user.#{id}@anonymizable.io" }
4
+ password { random_password }
5
+ role
6
+ end
7
+
8
+ factory :admin, class: User do
9
+ email { "admin.user.#{id}@anonymizable.io" }
10
+ password { random_password }
11
+ role { FactoryGirl.create(:admin_role) }
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ class Avatar < ActiveRecord::Base
2
+ self.table_name = :images
3
+
4
+ belongs_to :user, foreign_key: :profile_id
5
+
6
+ end
@@ -0,0 +1,8 @@
1
+ class Comment < ActiveRecord::Base
2
+
3
+ belongs_to :post
4
+ belongs_to :user
5
+
6
+ anonymizable :user_id
7
+
8
+ end
@@ -0,0 +1,6 @@
1
+ class Image < ActiveRecord::Base
2
+
3
+ belongs_to :user
4
+ has_many :thumbnails, class_name: "Image"
5
+
6
+ end
@@ -0,0 +1,8 @@
1
+ class Like < ActiveRecord::Base
2
+
3
+ belongs_to :user
4
+ belongs_to :post
5
+
6
+ anonymizable :user_id
7
+
8
+ end
@@ -0,0 +1,10 @@
1
+ class Post < ActiveRecord::Base
2
+
3
+ belongs_to :user
4
+ has_many :comments
5
+ has_many :images
6
+ has_many :likes
7
+
8
+ anonymizable :user_id
9
+
10
+ end
@@ -0,0 +1,11 @@
1
+ class Role < ActiveRecord::Base
2
+
3
+ has_many :users
4
+
5
+ def method_missing(method_name, *args, &block)
6
+ if /[a-z]+\?/.match(method_name)
7
+ return name == method_name.to_s.sub(/\?/,"")
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,40 @@
1
+ class User < ActiveRecord::Base
2
+
3
+ anonymizable public: true, raise_on_delete: true do
4
+ only_if :can_anonymize?
5
+
6
+ attributes :first_name, :last_name, :profile,
7
+ email: Proc.new {|u| "anonymized.user.#{u.id}@foobar.com" },
8
+ password: :random_password
9
+
10
+ associations do
11
+ anonymize :posts, :comments
12
+ delete :avatar, :likes
13
+ destroy :images
14
+ end
15
+
16
+ after :email_user, :email_admin
17
+ end
18
+
19
+ belongs_to :role
20
+ has_many :posts
21
+ has_many :comments
22
+ has_one :avatar, foreign_key: :profile_id
23
+ has_many :images
24
+ has_many :likes
25
+
26
+ def can_anonymize?
27
+ !role.admin?
28
+ end
29
+
30
+ def random_password
31
+ SecureRandom.hex.sub(/([a-z])/) {|s| s.upcase}
32
+ end
33
+
34
+ def email_user(original_attributes)
35
+ end
36
+
37
+ def email_admin(original_attributes)
38
+ end
39
+
40
+ end
@@ -0,0 +1,50 @@
1
+ ActiveRecord::Schema.define(version: 0) do
2
+
3
+ create_table :users, force: true do |t|
4
+ t.string :first_name
5
+ t.string :last_name
6
+ t.string :email, null: false
7
+ t.string :password, null: false
8
+ t.integer :role_id, null: false
9
+ t.string :profile
10
+ t.timestamps null: false
11
+ end
12
+
13
+ create_table :roles, force: true do |t|
14
+ t.string :name, null: false
15
+ t.timestamps null: false
16
+ end
17
+
18
+ create_table :posts, force: true do |t|
19
+ t.integer :user_id
20
+ t.string :title, null: false
21
+ t.string :summary, null: false
22
+ t.text :content, null: false
23
+ t.timestamps null: false
24
+ end
25
+
26
+ create_table :comments, force: true do |t|
27
+ t.integer :user_id
28
+ t.integer :post_id, null: false
29
+ t.text :text, null: false
30
+ t.timestamps null: false
31
+ end
32
+
33
+ create_table :likes, force: true do |t|
34
+ t.integer :user_id
35
+ t.integer :post_id, null: false
36
+ t.timestamps null: false
37
+ end
38
+
39
+ create_table :images, force: true do |t|
40
+ t.integer :image_id
41
+ t.integer :user_id
42
+ t.integer :profile_id
43
+ t.integer :width
44
+ t.integer :height
45
+ t.binary :data
46
+ t.string :caption
47
+ t.timestamps null: false
48
+ end
49
+
50
+ end
@@ -0,0 +1,40 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
2
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
3
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'models')))
4
+
5
+ require 'anonymizable'
6
+ require 'logger'
7
+ require 'rspec'
8
+ require 'database_cleaner'
9
+ require 'factory_girl'
10
+ require 'yaml'
11
+
12
+ def load_schema
13
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
14
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
15
+ ActiveRecord::Base.establish_connection(config['sqlite3'])
16
+ load(File.dirname(__FILE__) + "/schema.rb")
17
+ end
18
+
19
+ def load_models
20
+ Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), 'models/*.rb'))).each {|f| require f}
21
+ Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), 'factories/*.rb'))).each {|f| require f}
22
+ end
23
+
24
+ load_schema
25
+ load_models
26
+
27
+ RSpec.configure do |config|
28
+ config.color = true
29
+ config.formatter = :documentation
30
+ config.include FactoryGirl::Syntax::Methods
31
+
32
+ config.before(:suite) do
33
+ DatabaseCleaner.strategy = :transaction
34
+ DatabaseCleaner.clean_with(:truncation)
35
+ end
36
+
37
+ config.after(:each) do |example|
38
+ DatabaseCleaner.clean
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anonymizable
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Mike Bradford
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-22 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: '3.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.2'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.2'
47
+ - !ruby/object:Gem::Dependency
48
+ name: sqlite3
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.3'
61
+ - !ruby/object:Gem::Dependency
62
+ name: database_cleaner
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: factory_girl
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '4.5'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '4.5'
89
+ - !ruby/object:Gem::Dependency
90
+ name: pry
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.10'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.10'
103
+ description: Delete data without deleting it
104
+ email:
105
+ - mbradford@47primes.com
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - ".gitignore"
111
+ - ".pryrc"
112
+ - Gemfile
113
+ - README.md
114
+ - anonymizable.gemspec
115
+ - lib/anonymizable.rb
116
+ - lib/anonymizable/configuration.rb
117
+ - lib/anonymizable/version.rb
118
+ - spec/anonymizable_spec.rb
119
+ - spec/configuration_spec.rb
120
+ - spec/database.yml
121
+ - spec/factories/comment_factory.rb
122
+ - spec/factories/image_factory.rb
123
+ - spec/factories/like_factory.rb
124
+ - spec/factories/post_factory.rb
125
+ - spec/factories/role_factory.rb
126
+ - spec/factories/user_factory.rb
127
+ - spec/models/avatar.rb
128
+ - spec/models/comment.rb
129
+ - spec/models/image.rb
130
+ - spec/models/like.rb
131
+ - spec/models/post.rb
132
+ - spec/models/role.rb
133
+ - spec/models/user.rb
134
+ - spec/schema.rb
135
+ - spec/spec_helper.rb
136
+ homepage: https://github.com/47primes/anonymizable
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.4.6
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Anonymize columns in ActiveRecord models
160
+ test_files: []