anony 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +86 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +28 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +64 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +451 -0
- data/anony.gemspec +38 -0
- data/docs/COMPATIBILITY.md +17 -0
- data/lib/anony.rb +12 -0
- data/lib/anony/anonymisable.rb +80 -0
- data/lib/anony/config.rb +42 -0
- data/lib/anony/cops.rb +3 -0
- data/lib/anony/cops/define_deletion_strategy.rb +54 -0
- data/lib/anony/duplicate_strategy_exception.rb +19 -0
- data/lib/anony/field_exception.rb +20 -0
- data/lib/anony/field_level_strategies.rb +155 -0
- data/lib/anony/model_config.rb +102 -0
- data/lib/anony/result.rb +39 -0
- data/lib/anony/rspec_shared_examples.rb +45 -0
- data/lib/anony/skipped_exception.rb +9 -0
- data/lib/anony/strategies/destroy.rb +39 -0
- data/lib/anony/strategies/overwrite.rb +173 -0
- data/lib/anony/version.rb +5 -0
- metadata +194 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: eee3daa49f033d44737b37aee49c2281430cc23a4ea770c4c0891684ed47fbf8
|
4
|
+
data.tar.gz: 2304bfd91b8f503e7c562db4bed2994da418e78180ed11f629630c4122142982
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7fca58237bbd4a8e22217ac9547475f4f13483532a05ac6fe1a02a3e93bd047f965734b0eca08fb5690fde36ac7b3d1b94129bcffa822c31ed5fc78b93be04ea
|
7
|
+
data.tar.gz: 2eed4ea29fe1c960bff89b2fa41e63a8338c69069d1dea353a675f4a2e6b1e6e8cb62d4a1be7c7b4d8a445b933870eb24f3dfae920bfbef83b5a9daeccc3a640
|
@@ -0,0 +1,86 @@
|
|
1
|
+
version: 2
|
2
|
+
|
3
|
+
references:
|
4
|
+
steps: &steps
|
5
|
+
- checkout
|
6
|
+
|
7
|
+
- type: cache-restore
|
8
|
+
key: anony-bundler-{{ checksum "anony.gemspec" }}
|
9
|
+
|
10
|
+
- run: gem install bundler -v 2.1.4
|
11
|
+
- run: bundle config set path 'vendor/bundle'
|
12
|
+
- run: bundle install
|
13
|
+
|
14
|
+
- type: cache-save
|
15
|
+
key: anony-bundler-{{ checksum "anony.gemspec" }}
|
16
|
+
paths:
|
17
|
+
- vendor/bundle
|
18
|
+
|
19
|
+
- type: shell
|
20
|
+
command: |
|
21
|
+
bundle exec rspec --profile 10 \
|
22
|
+
--format RspecJunitFormatter \
|
23
|
+
--out /tmp/test-results/rspec.xml \
|
24
|
+
--format progress \
|
25
|
+
spec
|
26
|
+
|
27
|
+
- type: store_test_results
|
28
|
+
path: /tmp/test-results
|
29
|
+
|
30
|
+
- run: bundle exec rubocop --parallel --extra-details --display-style-guide
|
31
|
+
|
32
|
+
jobs:
|
33
|
+
ruby24-rails52:
|
34
|
+
docker:
|
35
|
+
- image: ruby:2.4
|
36
|
+
environment:
|
37
|
+
- RAILS_VERSION=5.2
|
38
|
+
steps: *steps
|
39
|
+
ruby25-rails52:
|
40
|
+
docker:
|
41
|
+
- image: ruby:2.5
|
42
|
+
environment:
|
43
|
+
- RAILS_VERSION=5.2
|
44
|
+
steps: *steps
|
45
|
+
ruby25-rails60:
|
46
|
+
docker:
|
47
|
+
- image: ruby:2.5
|
48
|
+
environment:
|
49
|
+
- RAILS_VERSION=6.0
|
50
|
+
steps: *steps
|
51
|
+
ruby26-rails52:
|
52
|
+
docker:
|
53
|
+
- image: ruby:2.6
|
54
|
+
environment:
|
55
|
+
- RAILS_VERSION=5.2
|
56
|
+
steps: *steps
|
57
|
+
ruby26-rails60:
|
58
|
+
docker:
|
59
|
+
- image: ruby:2.6
|
60
|
+
environment:
|
61
|
+
- RAILS_VERSION=6.0
|
62
|
+
steps: *steps
|
63
|
+
ruby27-rails52:
|
64
|
+
docker:
|
65
|
+
- image: ruby:2.7
|
66
|
+
environment:
|
67
|
+
- RAILS_VERSION=5.2
|
68
|
+
steps: *steps
|
69
|
+
ruby27-rails60:
|
70
|
+
docker:
|
71
|
+
- image: ruby:2.7
|
72
|
+
environment:
|
73
|
+
- RAILS_VERSION=6.0
|
74
|
+
steps: *steps
|
75
|
+
|
76
|
+
workflows:
|
77
|
+
version: 2
|
78
|
+
tests:
|
79
|
+
jobs:
|
80
|
+
- ruby24-rails52
|
81
|
+
- ruby25-rails52
|
82
|
+
- ruby25-rails60
|
83
|
+
- ruby26-rails52
|
84
|
+
- ruby26-rails60
|
85
|
+
- ruby27-rails52
|
86
|
+
- ruby27-rails60
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2019-11-11 12:18:14 +0000 using RuboCop version 0.76.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
RSpec/LeakyConstantDeclaration:
|
10
|
+
Exclude:
|
11
|
+
- 'spec/anony/anonymisable_spec.rb'
|
12
|
+
- 'spec/anony/strategies/overwrite_spec.rb'
|
13
|
+
|
14
|
+
RSpec/ExampleLength:
|
15
|
+
Exclude:
|
16
|
+
- 'spec/anony/anonymisable_spec.rb'
|
17
|
+
|
18
|
+
RSpec/NestedGroups:
|
19
|
+
Exclude:
|
20
|
+
- 'spec/anony/anonymisable_spec.rb'
|
21
|
+
|
22
|
+
RSpec/FilePath:
|
23
|
+
Exclude:
|
24
|
+
- 'spec/anony/cops/define_deletion_strategy_spec.rb'
|
25
|
+
|
26
|
+
RSpec/DescribeClass:
|
27
|
+
Exclude:
|
28
|
+
- 'spec/anony/rspec_shared_examples_spec.rb'
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.5
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# v1.0.0
|
2
|
+
|
3
|
+
* Create a result object when calling `anonymise!` [#44](https://github.com/gocardless/anony/pull/44)
|
4
|
+
|
5
|
+
# v0.8.0
|
6
|
+
|
7
|
+
* Improve the documentation [#45](https://github.com/gocardless/anony/pull/45)
|
8
|
+
* Rename fields strategy to overwrite [#46](https://github.com/gocardless/anony/pull/46)
|
9
|
+
|
10
|
+
# v0.7.3
|
11
|
+
|
12
|
+
* Allow customising the model superclass for the `DefineDeletionStrategy` cop [#36](https://github.com/gocardless/anony/pull/36)
|
13
|
+
|
14
|
+
# v0.7.2
|
15
|
+
|
16
|
+
* Add ability to prevent anonymisation with `skip_if` [#25](https://github.com/gocardless/anony/pull/25)
|
17
|
+
|
18
|
+
# v0.7.1
|
19
|
+
|
20
|
+
* Fix breakage when applying a strategy multiple times [#35](https://github.com/gocardless/anony/pull/35)
|
21
|
+
|
22
|
+
# v0.7.0
|
23
|
+
|
24
|
+
* **BREAKING** Switch to nesting field-level configuration in a `fields` block
|
25
|
+
[#32](https://github.com/gocardless/anony/pull/32). This should just be a case of
|
26
|
+
switching `anonymise { ... }` to `anonymise { fields { ... } }` in most cases, but for
|
27
|
+
more details please check the README.
|
28
|
+
* **BREAKING** `Anony::Strategies.register` was renamed to `Anony::FieldLevelStrategies.register`.
|
29
|
+
|
30
|
+
# v0.6.0
|
31
|
+
|
32
|
+
* Use ActiveRecord::Persistence#current_time_from_proper_timezone [#34](https://github.com/gocardless/anony/pull/34)
|
33
|
+
|
34
|
+
# v0.5.0
|
35
|
+
|
36
|
+
* Make `valid_anonymisation?` a class method [#24](https://github.com/gocardless/anony/pull/24)
|
37
|
+
* Allow dynamic registration of Anony::Strategies [#23](https://github.com/gocardless/anony/pull/23)
|
38
|
+
* Only apply anonymisation strategies to columns that are defined [#28](https://github.com/gocardless/anony/pull/28)
|
39
|
+
|
40
|
+
# v0.4.0
|
41
|
+
|
42
|
+
* Allow using a constant value as a strategy [#19](https://github.com/gocardless/anony/pull/19)
|
43
|
+
|
44
|
+
# v0.3.1
|
45
|
+
|
46
|
+
* Fix `anonymised_at` column [#13](https://github.com/gocardless/anony/pull/13)
|
47
|
+
|
48
|
+
# v0.3.0
|
49
|
+
|
50
|
+
* Support `anonymised_at` column [#9](https://github.com/gocardless/anony/pull/9)
|
51
|
+
|
52
|
+
# v0.2.1
|
53
|
+
|
54
|
+
* Fix relative require in DefineDeletionStrategy cop [#8](https://github.com/gocardless/anony/pull/8)
|
55
|
+
|
56
|
+
# v0.2
|
57
|
+
|
58
|
+
* Improve the README [#5](https://github.com/gocardless/anony/pulls/5)
|
59
|
+
* Use Rubocop for testing code style [#6](https://github.com/gocardless/anony/pulls/6)
|
60
|
+
* Add an [RSpec helper](https://github.com/gocardless/anony/blob/v0.2/README.md#testing) for testing [#7](https://github.com/gocardless/anony/pulls/7)
|
61
|
+
|
62
|
+
# v0.1
|
63
|
+
|
64
|
+
Initial release.
|
data/Gemfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
6
|
+
|
7
|
+
# Specify your gem's dependencies in anony.gemspec
|
8
|
+
gemspec
|
9
|
+
|
10
|
+
gem "activerecord", "~> #{ENV['RAILS_VERSION']}" if ENV["RAILS_VERSION"]
|
11
|
+
gem "activesupport", "~> #{ENV['RAILS_VERSION']}" if ENV["RAILS_VERSION"]
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2019 GoCardless
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,451 @@
|
|
1
|
+
# Anony
|
2
|
+
|
3
|
+
Anony is a small library that defines how ActiveRecord models should be anonymised for
|
4
|
+
deletion purposes.
|
5
|
+
|
6
|
+
```ruby
|
7
|
+
class User < ActiveRecord::Base
|
8
|
+
include Anony::Anonymisable
|
9
|
+
|
10
|
+
anonymise do
|
11
|
+
overwrite do
|
12
|
+
hex :first_name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
```
|
17
|
+
```ruby
|
18
|
+
irb(main):001:0> user = User.find(1)
|
19
|
+
=> #<User id="1" first_name="Alice">
|
20
|
+
|
21
|
+
irb(main):002:0> user.anonymise!
|
22
|
+
=> #<Anony::Result status="overwritten" fields=[:first_name] error=nil>
|
23
|
+
```
|
24
|
+
|
25
|
+
For our policy on compatibility with Ruby and Rails versions, see [COMPATIBILITY.md](docs/COMPATIBILITY.md).
|
26
|
+
|
27
|
+
## Installation & configuration
|
28
|
+
|
29
|
+
This library is distributed as a Ruby gem, and we recommend adding it your Gemfile:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
gem "anony"
|
33
|
+
```
|
34
|
+
|
35
|
+
The library injects itself using a mixin. To add this to a model class, you should include
|
36
|
+
`Anony::Anonymisable`:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class User < ActiveRecord::Base
|
40
|
+
include Anony::Anonymisable
|
41
|
+
# ...
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
Alternatively, if you have a Rails application, you might wish to expose this behaviour
|
46
|
+
for all of your models: in which case, you can instead add it to `ApplicationRecord` once:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# app/models/application_record.rb
|
50
|
+
class ApplicationRecord < ActiveRecord::Base
|
51
|
+
include Anony::Anonymisable
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
## Usage
|
56
|
+
|
57
|
+
There are two primary ways to use this library: to either overwrite existing fields on a
|
58
|
+
record, or to destroy the record altogether.
|
59
|
+
|
60
|
+
First, you should establish an `anonymise` block in your model class:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class Employee < ActiveRecord::Base
|
64
|
+
include Anony::Anonymisable
|
65
|
+
|
66
|
+
anonymise do
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
If you want to overwrite certain fields on the model, you should use the `overwrite`
|
72
|
+
DSL. There are many different ways (known as "strategies") to overwrite your fields (see
|
73
|
+
[Field strategies](#field-strategies) below). For now, let's use the `hex` & `nilable` strategies, which
|
74
|
+
overwrites fields using `SecureRandom.hex` or sets them to `nil`:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
anonymise do
|
78
|
+
overwrite do
|
79
|
+
hex :field_name
|
80
|
+
nilable :nullable_field
|
81
|
+
end
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
Alternative, you may wish to simply destroy the record altogether when we call
|
86
|
+
`#anonymise!` (this is useful if you're anonymising a collection of different models
|
87
|
+
together, only some of which need to be destroyed). This can be configured liked so:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
anonymise do
|
91
|
+
destroy
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
Please note that both the `overwrite` and `destroy` strategies cannot be used simultaneously.
|
96
|
+
|
97
|
+
Now, given a model instance, we can use the `#anonymise!` method to apply our strategies:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
irb(main):001:0> model = Model.find(1)
|
101
|
+
=> #<Model id="1" field_name="Previous value" nullable_field="Previous">
|
102
|
+
|
103
|
+
irb(main):002:0> model.anonymise!
|
104
|
+
=> #<Anony::Result status="overwritten" fields=[:field_name, :nullable_field] error=nil>
|
105
|
+
```
|
106
|
+
|
107
|
+
Or, if you were using the `destroy` strategy:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
irb(main):002:0> model.anonymise!
|
111
|
+
=> #<Anony::Result status="destroyed" fields=nil error=nil>
|
112
|
+
```
|
113
|
+
|
114
|
+
### Result object
|
115
|
+
|
116
|
+
When a model is anonymised, an `Anony::Result` is returned. This allows the library to detail the changes is made and the strategy it used. The result object also contains the errors that may have been raised within Anony, allowing you to handle them elegantly without using the exceptions for flow control.
|
117
|
+
|
118
|
+
The result object has 3 attributes:
|
119
|
+
|
120
|
+
* `status` - If the model was `destroyed`, `overwritten`, `skipped` or the operation `failed`
|
121
|
+
* `fields` - In the event the model was `overwritten`, the fields that were updated (excludes timestamps)
|
122
|
+
* `error` - In the event the anonymisation `failed`, then the associated error. Note only rescues the following errors: `ActiveRecord::RecordNotSaved`, `ActiveRecord::RecordNotDestroyed`. Anything else is thrown.
|
123
|
+
|
124
|
+
For convenience, the result object can also be queried with `destroyed?`, `overwritten?`, `skipped?` and `failed?`, so that it can be directly interrogated or used in a `switch case` with the `status` property.
|
125
|
+
|
126
|
+
### Field strategies
|
127
|
+
|
128
|
+
This library ships with a number of built-in strategies:
|
129
|
+
|
130
|
+
* **nilable** overwrites the field with `nil`
|
131
|
+
* **hex** overwrites the field with random hexadecimal characters
|
132
|
+
* **email** overwrites the field with an email
|
133
|
+
* **phone_number** overwrites the field with a dummy phone number
|
134
|
+
* **current_datetime** overwrites the field with `Time.zone.now` (using [ActiveSupport's TimeWithZone](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-now))
|
135
|
+
|
136
|
+
### Custom strategies
|
137
|
+
|
138
|
+
You can override the default strategies, or add your own ones to make them available
|
139
|
+
everywhere, using the `Anony::FieldLevelStrategies.register(name, &block)` method somewhere after
|
140
|
+
your application boots (e.g. in a Rails initializer):
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
Anony::FieldLevelStrategies.register(:reverse) do |original|
|
144
|
+
original.reverse
|
145
|
+
end
|
146
|
+
|
147
|
+
class Employee < ApplicationRecord
|
148
|
+
include Anony::Anonymisable
|
149
|
+
|
150
|
+
anonymise do
|
151
|
+
overwrite do
|
152
|
+
reverse :first_name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
> One strategy you might want to override is `:email`, if your application has a more
|
159
|
+
> specific replacement. For example, at GoCardless we use an email on the
|
160
|
+
> `@gocardless.com` domain so we can ensure any emails accidentally sent to this address
|
161
|
+
> would be quickly identified and fixed. `:phone_number` is another strategy that you
|
162
|
+
> might wish to replace (depending on your primary location).
|
163
|
+
|
164
|
+
You can also use strategies on a case-by-case basis, by honouring the
|
165
|
+
`.call(existing_value)` signature:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
module OverwriteUUID
|
169
|
+
def self.call(_existing_value)
|
170
|
+
SecureRandom.uuid
|
171
|
+
end
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
require "overwrite_uuid"
|
177
|
+
|
178
|
+
class Manager < ApplicationRecord
|
179
|
+
include Anony::Anonymisable
|
180
|
+
|
181
|
+
anonymise do
|
182
|
+
overwrite do
|
183
|
+
with_strategy OverwriteUUID, :id
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
If your strategy doesn't respond to `.call`, then it will be used as a constant value
|
190
|
+
whenever the field is anonymised.
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class Manager < ApplicationRecord
|
194
|
+
include Anony::Anonymisable
|
195
|
+
|
196
|
+
anonymise do
|
197
|
+
overwrite do
|
198
|
+
with_strategy 123, :id
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
```
|
205
|
+
irb(main):001:0> manager = Manager.first
|
206
|
+
=> #<Manager id=42>
|
207
|
+
|
208
|
+
irb(main):002:0> manager.anonymise!
|
209
|
+
=> #<Anony::Result status="overwritten" fields=[:id] error=nil>
|
210
|
+
|
211
|
+
irb(main):003:0> manager
|
212
|
+
=> #<Manager id=123>
|
213
|
+
```
|
214
|
+
|
215
|
+
You can also use a block, which is executed in the context of the model so it can
|
216
|
+
access local properties & methods. Blocks take the existing value of the column as the
|
217
|
+
only argument:
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
class Manager < ApplicationRecord
|
221
|
+
include Anony::Anonymisable
|
222
|
+
|
223
|
+
anonymise do
|
224
|
+
overwrite do
|
225
|
+
with_strategy(:first_name) { |name| Digest::SHA2.hexdigest(name) }
|
226
|
+
with_strategy(:last_name) { "previous-name-of-#{id}" }
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
```
|
233
|
+
irb(main):001:0> manager = Manager.first
|
234
|
+
=> #<Manager id=42>
|
235
|
+
|
236
|
+
irb(main):002:0> manager.anonymise!
|
237
|
+
=> #<Anony::Result status="overwritten" fields=[:first_name, :last_name] error=nil>
|
238
|
+
|
239
|
+
irb(main):003:0> manager
|
240
|
+
=> #<Manager first_name="e9ab2800-d4b9-4227-94a7-7f81118d8a8a" last_name="previous-name-of-42">
|
241
|
+
```
|
242
|
+
|
243
|
+
### Identifying anonymised records
|
244
|
+
|
245
|
+
If your model has an `anonymised_at` column, Anony will automatically set that value
|
246
|
+
when calling `#anonymise!` (similar to how Rails will modify the `updated_at` timestamp).
|
247
|
+
This means you could automatically filter out anonymised records without matching on the
|
248
|
+
anonymised values.
|
249
|
+
|
250
|
+
Here is an example of adding this column with new tables:
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
class AddEmployees < ActiveRecord::Migration[6.0]
|
254
|
+
def change
|
255
|
+
create_table(:employees) do |t|
|
256
|
+
# ... the rest of your columns
|
257
|
+
t.column :anonymised_at, :datetime, null: true
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
Here is an example of adding this column to an existing table:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class AddAnonymisedAtToEmployees < ActiveRecord::Migration[6.0]
|
267
|
+
def change
|
268
|
+
add_column(:employees, :anonymised_at, :datetime, null: true)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
```
|
272
|
+
|
273
|
+
Records can then be filtered out like so:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
class Employees < ApplicationRecord
|
277
|
+
scope :without_anonymised, -> { where(anonymised_at: nil) }
|
278
|
+
end
|
279
|
+
```
|
280
|
+
|
281
|
+
### Preventing anonymisation
|
282
|
+
|
283
|
+
You might have a need to preserve model data in some (or all) circumstances. Anony exposes
|
284
|
+
the `skip_if` DSL for expressing this preference, which runs the given block before
|
285
|
+
attempting any strategy.
|
286
|
+
|
287
|
+
* If the block returns _truthy_, anonymisation is skipped.
|
288
|
+
* If the block returns _falsey_, anonymisation continues.
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
class Manager
|
292
|
+
def should_not_be_anonymised?
|
293
|
+
id == 1 # The first manager must be kept
|
294
|
+
end
|
295
|
+
|
296
|
+
anonymise do
|
297
|
+
skip_if { should_not_be_anonymised? }
|
298
|
+
end
|
299
|
+
end
|
300
|
+
```
|
301
|
+
|
302
|
+
The result object will indicate the model was skipped:
|
303
|
+
|
304
|
+
```
|
305
|
+
irb(main):001:0> manager = Manager.find(1)
|
306
|
+
=> #<Manager id=1>
|
307
|
+
|
308
|
+
irb(main):002:0> manager.anonymise!
|
309
|
+
=> #<Anony::Result status="skipped" fields=[] error=nil>
|
310
|
+
```
|
311
|
+
|
312
|
+
## Incomplete field strategies
|
313
|
+
|
314
|
+
One of the goals of this library is to ensure that your field strategies are _complete_,
|
315
|
+
i.e. that the anonymisation behaviour of the model is always correct, even when database
|
316
|
+
columns are added/removed or the contents of those columns changes.
|
317
|
+
|
318
|
+
As such, Anony will validate your model configuration when you try to anonymise the
|
319
|
+
model (unfortunately this cannot be safely done at boot as the database might not be
|
320
|
+
available). If your configuration is incomplete, calling `#anonymise!` will raise a
|
321
|
+
`FieldsException` and will not return an `Anony:Result` object. This is perceived
|
322
|
+
to a critical error as anony cannot safely anonymise the model.
|
323
|
+
|
324
|
+
```
|
325
|
+
irb(main):001:0> manager = Manager.find(1)
|
326
|
+
=> #<Manager id=1>
|
327
|
+
|
328
|
+
irb(main):002:0> manager.anonymise!
|
329
|
+
Anony::FieldException (Invalid anonymisation strategy for field(s) [:username])
|
330
|
+
```
|
331
|
+
|
332
|
+
We recommend adding a test for each model that you anonymise (see [Testing](#testing)
|
333
|
+
below).
|
334
|
+
|
335
|
+
### Adding new columns
|
336
|
+
|
337
|
+
Anony will fail if you try to anonymise a model without specifying a
|
338
|
+
strategy for all of the columns (to ensure that anonymisation rules aren't missed over
|
339
|
+
time). However, it's fine to define a strategy for a column
|
340
|
+
that hasn't yet been added.
|
341
|
+
|
342
|
+
This means that, in order to add a new column, you should:
|
343
|
+
|
344
|
+
1. Define a strategy for the new column (e.g. `nilable :new_column`)
|
345
|
+
2. Add the column in a database migration.
|
346
|
+
|
347
|
+
> At GoCardless we do zero-downtime deploys so we would deploy the first change before
|
348
|
+
> then deploying the migration.
|
349
|
+
|
350
|
+
### Excluding common Rails columns
|
351
|
+
|
352
|
+
Rails applications typically have an `id`, `created_at` and `updated_at` column on all new
|
353
|
+
tables by default. To avoid anonymising these fields (and thus prevent a
|
354
|
+
`FieldsException`), they can be globally ignored:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
# config/initializers/anony.rb
|
358
|
+
|
359
|
+
Anony::Config.ignore_fields(:id, :created_at, :updated_at)
|
360
|
+
```
|
361
|
+
|
362
|
+
By default, `Config.ignore_fields` is an empty array and all fields are considered
|
363
|
+
anonymisable.
|
364
|
+
|
365
|
+
## Testing
|
366
|
+
|
367
|
+
This library ships with a set of useful RSpec examples for your specs. Just require them
|
368
|
+
somewhere before running your spec:
|
369
|
+
|
370
|
+
```ruby
|
371
|
+
require "anony/rspec_shared_examples"
|
372
|
+
```
|
373
|
+
|
374
|
+
```ruby
|
375
|
+
# spec/models/employee_spec.rb
|
376
|
+
|
377
|
+
RSpec.describe Employee do
|
378
|
+
# We use FactoryBot at GoCardless, but
|
379
|
+
# however you setup a model instance is fine
|
380
|
+
subject { FactoryBot.build(:employee) }
|
381
|
+
|
382
|
+
# If you just anonymise fields normally
|
383
|
+
it_behaves_like "overwritten anonymisable model"
|
384
|
+
|
385
|
+
# Or, if your anonymised model should be skipped
|
386
|
+
it_behaves_like "skipped anonymisable model"
|
387
|
+
|
388
|
+
# Or, if you anonymise by destroying the record
|
389
|
+
it_behaves_like "destroyed anonymisable model"
|
390
|
+
end
|
391
|
+
```
|
392
|
+
|
393
|
+
You can also override the subject _inside_ the shared example if it helps (e.g. if you
|
394
|
+
need to persist the record before anonymising it):
|
395
|
+
|
396
|
+
```ruby
|
397
|
+
RSpec.describe Employee do
|
398
|
+
it_behaves_like "anonymisable model with destruction" do
|
399
|
+
subject { FactoryBot.create(:employee) }
|
400
|
+
end
|
401
|
+
end
|
402
|
+
```
|
403
|
+
|
404
|
+
If you're not using RSpec, or want more control over the tests, Anony also exposes an
|
405
|
+
instance method called `#valid_anonymisation?`. A simple spec would be:
|
406
|
+
|
407
|
+
```ruby
|
408
|
+
RSpec.describe Employee do
|
409
|
+
subject { described_class.new }
|
410
|
+
|
411
|
+
it { is_expected.to be_valid_anonymisation }
|
412
|
+
end
|
413
|
+
```
|
414
|
+
|
415
|
+
## Integration with Rubocop
|
416
|
+
|
417
|
+
At GoCardless, we use Rubocop heavily to ensure consistency in our applications. This
|
418
|
+
library includes some Rubocop cops, which can be used by adding `anony/cops` to the
|
419
|
+
`require` list in your `.rubocop.yml`:
|
420
|
+
|
421
|
+
```yml
|
422
|
+
require:
|
423
|
+
- anony/cops
|
424
|
+
```
|
425
|
+
|
426
|
+
### `Lint/DefineDeletionStrategy`
|
427
|
+
|
428
|
+
This cop ensures that all models in your application have defined an `anonymise` block.
|
429
|
+
The output looks like this:
|
430
|
+
|
431
|
+
```
|
432
|
+
app/models/employee.rb:7:1: W: Lint/DefineDeletionStrategy:
|
433
|
+
Define .anonymise for Employee, see https://github.com/gocardless/anony/blob/master/README.md for details:
|
434
|
+
class Employee < ApplicationRecord ...
|
435
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
436
|
+
```
|
437
|
+
|
438
|
+
If your models do not inherit from `ApplicationRecord`, you can specify their superclass
|
439
|
+
in your `.rubocop.yml`:
|
440
|
+
|
441
|
+
```yml
|
442
|
+
Lint/DefineDeletionStrategy:
|
443
|
+
ModelSuperclass: Acme::Record
|
444
|
+
```
|
445
|
+
|
446
|
+
## License & Contributing
|
447
|
+
|
448
|
+
* Anony is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
449
|
+
* Bug reports and pull requests are welcome on GitHub at https://github.com/gocardless/anony.
|
450
|
+
|
451
|
+
GoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/about/jobs).
|