anonymizable 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.pryrc +32 -0
- data/Gemfile +3 -0
- data/README.md +203 -0
- data/anonymizable.gemspec +25 -0
- data/lib/anonymizable.rb +150 -0
- data/lib/anonymizable/configuration.rb +100 -0
- data/lib/anonymizable/version.rb +3 -0
- data/spec/anonymizable_spec.rb +200 -0
- data/spec/configuration_spec.rb +101 -0
- data/spec/database.yml +3 -0
- data/spec/factories/comment_factory.rb +6 -0
- data/spec/factories/image_factory.rb +13 -0
- data/spec/factories/like_factory.rb +6 -0
- data/spec/factories/post_factory.rb +7 -0
- data/spec/factories/role_factory.rb +9 -0
- data/spec/factories/user_factory.rb +13 -0
- data/spec/models/avatar.rb +6 -0
- data/spec/models/comment.rb +8 -0
- data/spec/models/image.rb +6 -0
- data/spec/models/like.rb +8 -0
- data/spec/models/post.rb +10 -0
- data/spec/models/role.rb +11 -0
- data/spec/models/user.rb +40 -0
- data/spec/schema.rb +50 -0
- data/spec/spec_helper.rb +40 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
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
data/README.md
ADDED
@@ -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
|
data/lib/anonymizable.rb
ADDED
@@ -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,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
|
data/spec/database.yml
ADDED
@@ -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
|
data/spec/models/like.rb
ADDED
data/spec/models/post.rb
ADDED
data/spec/models/role.rb
ADDED
data/spec/models/user.rb
ADDED
@@ -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
|
data/spec/schema.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|