gdpr_admin 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +315 -1
- data/app/jobs/gdpr_admin/data_retention_policies_runner_job.rb +11 -0
- data/app/jobs/gdpr_admin/request_processor_job.rb +2 -0
- data/app/models/concerns/gdpr_admin/data_retention_policy_concern.rb +35 -0
- data/app/models/gdpr_admin/data_retention_policy.rb +7 -0
- data/app/models/gdpr_admin/request.rb +29 -8
- data/db/migrate/20230212185817_create_gdpr_admin_requests.rb +1 -1
- data/db/migrate/20230216171202_create_gdpr_admin_data_retention_policies.rb +14 -0
- data/lib/gdpr_admin/anonymizers/company_anonymizer.rb +11 -0
- data/lib/gdpr_admin/anonymizers/contact_anonymizer.rb +28 -0
- data/lib/gdpr_admin/anonymizers/internet_anonymizer.rb +4 -0
- data/lib/gdpr_admin/application_data_policy.rb +1 -0
- data/lib/gdpr_admin/configuration.rb +5 -1
- data/lib/gdpr_admin/error.rb +5 -0
- data/lib/gdpr_admin/helpers/erase_helper.rb +9 -35
- data/lib/gdpr_admin/helpers/field_anonymizer_helper.rb +59 -0
- data/lib/gdpr_admin/helpers/paper_trail_helper.rb +65 -0
- data/lib/gdpr_admin/helpers/scope_helper.rb +11 -0
- data/lib/gdpr_admin/invalid_status_error.rb +5 -0
- data/lib/gdpr_admin/paper_trail/version_data_policy.rb +42 -0
- data/lib/gdpr_admin/version.rb +3 -1
- data/lib/gdpr_admin.rb +4 -0
- metadata +27 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b5cc14d1e0f88e99bec3ccd100795cc1985ef86fa559fbdd2639bdd5bef1ee4
|
4
|
+
data.tar.gz: 5c07836dec53a6265e3fcd6516f910a22b6834234bea5f39890030ba394d0e5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4552232540bf1153c32e15ccc1ec0ed5f5a399649bd5618ded36476881124d2496a1f345deec8f5baf98ecba1ff4e0161743ca905abae45761024d9c68432e0
|
7
|
+
data.tar.gz: 8169ff0b5040484ceac0084601246d19d875e222509ec2f1a2dab055822d79db13b999c6a96468167cf39adfe5cc26b0aff60b9c046bbcc31422177e2ca66684
|
data/README.md
CHANGED
@@ -1,6 +1,23 @@
|
|
1
|
+
<p align="center">
|
2
|
+
<a href="http://github.com/Colex/gdpr_admin" target="blank"><img src="https://i.ibb.co/mJwpsFY/logo.png" width="120" alt="GDPR and Ruby logo" /></a>
|
3
|
+
</p>
|
4
|
+
|
1
5
|
# GDPR Admin
|
6
|
+
|
7
|
+
[](https://github.com/Colex/gdpr_admin/actions/workflows/build.yml)
|
8
|
+
[](https://codeclimate.com/github/Colex/gdpr_admin)
|
9
|
+
[](https://codeclimate.com/github/Colex/gdpr_admin/test_coverage)
|
10
|
+
[](http://badge.fury.io/rb/gdpr_admin)
|
11
|
+
|
2
12
|
Rails engine for processing GDPR processes. GDPR Admin offers a simple interface for defining strategies for automating the process of data access and data removal as required by data privacy regulations like GDPR.
|
3
13
|
|
14
|
+
GDPR Admin uses simple Ruby classes, so you're free to code your Data Policies as you see fit. A swiss knife of
|
15
|
+
helper methods are available to make the processes even simpler. The focus of the gem is to easily implement:
|
16
|
+
|
17
|
+
- [Right of access](https://ico.org.uk/for-organisations/guide-to-data-protection/guide-to-the-general-data-protection-regulation-gdpr/individual-rights/right-of-access/): export a subject's data on request;
|
18
|
+
- [Right to erasure](https://ico.org.uk/for-organisations/guide-to-data-protection/guide-to-the-general-data-protection-regulation-gdpr/individual-rights/right-to-erasure/): remove a subject's personal data on request;
|
19
|
+
- [Storage limitation](https://ico.org.uk/for-organisations/guide-to-data-protection/guide-to-the-general-data-protection-regulation-gdpr/principles/storage-limitation/): hold data for as long as required (data retention policies and offboarding tenants);
|
20
|
+
|
4
21
|
## Installation
|
5
22
|
Add this line to your application's Gemfile:
|
6
23
|
|
@@ -48,13 +65,310 @@ Once you have all your data policies defined, create a `GdprAdmin::Request` to p
|
|
48
65
|
GdprAdmin::Request.create!(
|
49
66
|
tenant: current_tenant,
|
50
67
|
requester: current_admin_user,
|
51
|
-
request_type: '
|
68
|
+
request_type: 'erase_data',
|
52
69
|
data_older_than: 1.month.ago, # Optional: by default, it will be todays date
|
53
70
|
)
|
54
71
|
```
|
55
72
|
|
56
73
|
Creating a request will automatically enqueue the request to be processed in 4 hours - this gives time to cancel an accidental request. You can configure this grace period as desired.
|
57
74
|
|
75
|
+
### Scope Helpers
|
76
|
+
Helpers to be used within the `#scope` method.
|
77
|
+
|
78
|
+
#### `scope_by_date`
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
scope_by_date(scope, field = :updated_at)
|
82
|
+
```
|
83
|
+
|
84
|
+
Automatically scopes the data using the `updated_at` column to match the GDPR Request. You can use a different column by providing
|
85
|
+
a second argument.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class ContactDataPolicy < GdprAdmin::ApplicationDataPolicy
|
89
|
+
def scope
|
90
|
+
scope_by_date(Contact)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
### Anonymization Helpers
|
96
|
+
A set of helper methods are available to make the anonymization even simpler. These are not mandatory, but can help you
|
97
|
+
keep your code cleaner and, better, write less code.
|
98
|
+
|
99
|
+
#### `erase_fields`
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
erase_fields(record, fields, base_changes = {})
|
103
|
+
```
|
104
|
+
|
105
|
+
The method `erase_fields` is available in the Data Policy class. It expects an array of field anonymization options.
|
106
|
+
It will automatically process those fields and update the record in the database using `update_columns` (so validations
|
107
|
+
are skipped). The last optional argument (`base_changes`) is a hash of attributes that should be updated when the record
|
108
|
+
is updated. See the example below:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class ContactDataPolicy < GdprAdmin::ApplicationDataPolicy
|
112
|
+
def fields
|
113
|
+
[
|
114
|
+
{ field: :first_name, method: :anonymize_first_name },
|
115
|
+
{ field: :last_name, method: :anonymize_last_name },
|
116
|
+
{ field: :gender, method: :skip },
|
117
|
+
{
|
118
|
+
field: :email,
|
119
|
+
method: lambda { |contact|
|
120
|
+
domain = contact.email[/@.*/]
|
121
|
+
"anonymous.contact#{contact.id}#{domain}"
|
122
|
+
},
|
123
|
+
},
|
124
|
+
{ field: :street_address, method: :nilify },
|
125
|
+
{ field: :city, method: :anonymize_city, seed: :id },
|
126
|
+
]
|
127
|
+
end
|
128
|
+
|
129
|
+
def erase(contact)
|
130
|
+
erase_fields(contact, fields, { anonymized_at: Time.zone.now })
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
The anonymizers used above (e.g. `anonymize_first_name`), by default, will use the value of the field being updated as
|
136
|
+
the seed. That means that, when anonymization with the same anonymizer function, equal values will **always** yield the
|
137
|
+
same anonymized value. _(note: different values may also yield the same value)_
|
138
|
+
|
139
|
+
To use the built-in anonymizer functions, you need to install the gem `faker`.
|
140
|
+
|
141
|
+
## GDPR Request
|
142
|
+
A GDPR Request (`GdprAdmin::Request`) represents a request to remove a subject's data, tenant's data, or export subject data.
|
143
|
+
|
144
|
+
### Model `GdprAdmin::Request`
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
GdprAdmin::Request.create!(
|
148
|
+
tenant: current_tenant,
|
149
|
+
requester: current_admin_user,
|
150
|
+
request_type: 'erase_data',
|
151
|
+
data_older_than: 30.days.ago,
|
152
|
+
)
|
153
|
+
```
|
154
|
+
|
155
|
+
#### `#tenant`
|
156
|
+
Defines which tenant is this request attributed to. All requests must be assigned to a Tenant.
|
157
|
+
|
158
|
+
#### `#request_type`
|
159
|
+
Represents what type of request is being performed. Must be one of:
|
160
|
+
- `erase_data`: erase a tenant's data;
|
161
|
+
- `erase_subject`: erase a single subject's data in a tenant;
|
162
|
+
- `export_subject`: export data about a subject within a tenant;
|
163
|
+
|
164
|
+
#### `#data_older_than`
|
165
|
+
The request should only affect any data created before than date defined here (e.g. erase anything older than 30 days ago).
|
166
|
+
|
167
|
+
#### `#process!`
|
168
|
+
This method executes the request. This is automatically called by the `GdprAdmin::RequestProcessorJob`.
|
169
|
+
|
170
|
+
## Data Retention Policies
|
171
|
+
Some tenants may have different data retention policies (e.g. some hold Personally identifiable information for 3 months,
|
172
|
+
others for 1 month).
|
173
|
+
|
174
|
+
### Setup Job
|
175
|
+
Run the job `GdprAdmin::DataRetentionPoliciesRunnerJob` periodically to execute your data retention policy (it is recommended
|
176
|
+
to run it, at least, once a day).
|
177
|
+
|
178
|
+
#### Sidekiq
|
179
|
+
You can define a repeating job with `sidekiq-cron`:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
Sidekiq::Cron::Job.create(
|
183
|
+
class: 'GdprAdmin::DataRetentionPoliciesRunnerJob',
|
184
|
+
cron: '0 2 * * *',
|
185
|
+
name: 'Run Data Retention Policies',
|
186
|
+
queue: 'cron',
|
187
|
+
active_job: true,
|
188
|
+
)
|
189
|
+
```
|
190
|
+
|
191
|
+
#### DelayedJobs
|
192
|
+
```ruby
|
193
|
+
GdprAdmin::DataRetentionPoliciesRunnerJob.set(cron: '0 2 * * *', queue: :cron).perform_later
|
194
|
+
```
|
195
|
+
|
196
|
+
### Model `GdprAdmin::DataRetentionPolicy`
|
197
|
+
|
198
|
+
This model allows you to create a custom data policy for each tenant and automatically run the policy periodically.
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
GdprAdmin::DataRetentionPolicy.create!(
|
202
|
+
tenant: acme_inc,
|
203
|
+
period_in_days: 30,
|
204
|
+
)
|
205
|
+
```
|
206
|
+
|
207
|
+
If you want to have a custom logic for running the data retention policy (e.g. do not run if the organization is deleted),
|
208
|
+
then you can add the logic to the method `#should_process?`:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
module GdprAdmin
|
212
|
+
class DataRetentionPolicy < GdprAdmin::ApplicationRecord
|
213
|
+
include GdprAdmin::DataRetentionPolicyConcern
|
214
|
+
|
215
|
+
def should_process?
|
216
|
+
tenant.deleted_at.nil?
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
|
223
|
+
## PaperTrail
|
224
|
+
GDPR Admin provides a set of tools to keep your PaperTrail GDPR Compliant.
|
225
|
+
|
226
|
+
### PaperTrail Data Privacy
|
227
|
+
By default, PaperTrail versions will not be anonymized. You may extend the default `PaperTrail::VersionDataPrivacy`
|
228
|
+
with your own scope. If you track custom fields with your versions (e.g. `ip`), then you can also define an anonymizer
|
229
|
+
for those here:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
# app/gdpr/paper_trail/version_data_privacy.rb
|
233
|
+
|
234
|
+
module PaperTrail
|
235
|
+
class VersionDataPolicy < GdprAdmin::PaperTrail::VersionDataPolicy
|
236
|
+
def fields
|
237
|
+
[
|
238
|
+
{ field: 'ip', method: :anonymize_ip },
|
239
|
+
]
|
240
|
+
end
|
241
|
+
|
242
|
+
def scope
|
243
|
+
return PaperTrail::Version.where(updated_at: ...request.data_older_than) if request.erase_data?
|
244
|
+
|
245
|
+
PaperTrail::Version.none
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
#### `PaperTrail::VersionDataPolicy#erase`
|
252
|
+
**NOTE:** this method only support JSON format for `object` and, optionally, `object_changes`. If you need a different
|
253
|
+
format, you will need to re-implement this method as desired.
|
254
|
+
|
255
|
+
`erase(version, item_fields = nil)`
|
256
|
+
|
257
|
+
The `erase` method will, by default, anonymize the data within `object` and `object_changes` (and whichever fields are
|
258
|
+
defined in the `#fields` method). It will choose which fields to anonymize the `object` and `object_changes` and which
|
259
|
+
anonymization methods by finding the `item`'s data policy and loading the fields from its `fields` method. Unless
|
260
|
+
`item_fields` is defined, in which case it will be used instead.
|
261
|
+
|
262
|
+
For example, if you have a `item_type` set to `User`, it will try to find the `UserDataPrivacy`. If you want to use a
|
263
|
+
different class for a `item_type`, you must define a `data_policy_class` in the model.
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class User < ApplicationRecord
|
267
|
+
def data_policy_class
|
268
|
+
PersonDataPolicy
|
269
|
+
end
|
270
|
+
end
|
271
|
+
```
|
272
|
+
|
273
|
+
If you'd like to just namespace all policies, then you can define `data_policy_prefix` in the `ApplicationRecord`:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
class ApplicationRecord < ActiveRecord::Base
|
277
|
+
def data_policy_prefix
|
278
|
+
'Gdpr::'
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Now, user should be defined in `Gdpr::UserDataPolicy`
|
283
|
+
```
|
284
|
+
|
285
|
+
### PaperTrail Helpers
|
286
|
+
When using the method `erase_fields`, no PaperTrail versions will be created in the database. GDPR Admin offer other
|
287
|
+
helper methods to deal with PaperTrail. (If you are not using `paper_trail`, this section may not be relevant)
|
288
|
+
|
289
|
+
#### `without_paper_trail`
|
290
|
+
|
291
|
+
Given a block, this method will execute it in a context where PaperTrail is disabled, so no versions are created:
|
292
|
+
```ruby
|
293
|
+
def erase(contact)
|
294
|
+
without_paper_trail do
|
295
|
+
contact.update!(first_name: 'John', last_name: 'Doe')
|
296
|
+
end
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
As mentioned above, this is **not** required when using `erase_fields` as it is the default behavior.
|
301
|
+
|
302
|
+
## Configuration
|
303
|
+
Configure GDPR Admin in a initializer file `config/initializers/gdpr_admin.rb`. The configuration should be done within
|
304
|
+
the block of `GdprAdmin.configure(&block)`:
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
# config/initializers/gdpr_admin/rb
|
308
|
+
GdprAdmin.configure do |config|
|
309
|
+
# GDPR Admin configuration here...
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
### Multi-Tenancy
|
314
|
+
GDPR Admin is built maily for B2B SaaS services and it assumes that the service may have multiple tenants. The
|
315
|
+
`GDPR::Request` object always expects a `tenant` to be provided. When processing the request, data will be automatically
|
316
|
+
segregated using the tenant adapter.
|
317
|
+
|
318
|
+
#### Tenant Class
|
319
|
+
By default, GDPR Admin will assume that the tenant class is `Organization`. You can change that by setting the
|
320
|
+
`tenant_class` in the config.
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
GdprAdmin.configure do |config|
|
324
|
+
config.tenant_class = 'Tenant'
|
325
|
+
end
|
326
|
+
```
|
327
|
+
|
328
|
+
#### ActsAsTenant Adapter
|
329
|
+
You can segregated the process to a tenant using `ActsAsTenant` gem:
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
GdprAdmin.configure do |config|
|
333
|
+
config.tenant_adapter = :acts_as_tenant
|
334
|
+
end
|
335
|
+
```
|
336
|
+
|
337
|
+
### Jobs
|
338
|
+
Requests are processed asynchronously using ActiveJob.
|
339
|
+
|
340
|
+
#### Custom Queue
|
341
|
+
You can set which queue should be used to schedule the job for processing the request using the config `default_job_queue`:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
GdprAdmin.configure do |config|
|
345
|
+
config.default_job_queue = :gdpr_tasks
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
#### Grace Periods
|
350
|
+
To allow for cancelling any accidental erasure request, the jobs are scheduled with a configurable grace period.
|
351
|
+
By default, erasure requests will wait 4 hours before being executed, while export requests will be executed immediately.
|
352
|
+
|
353
|
+
```ruby
|
354
|
+
GdprAdmin.configure do |config|
|
355
|
+
config.erasure_grace_period = 1.day
|
356
|
+
config.export_grace_period = 2.minutes
|
357
|
+
end
|
358
|
+
```
|
359
|
+
|
360
|
+
### Other Configurations
|
361
|
+
|
362
|
+
#### Data Policies Directory
|
363
|
+
By default, GDPR Admin will assume that your data policies are defined in `app/gdpr`. If you wish to have them
|
364
|
+
in a different place, you can change the option `data_policies_path`:
|
365
|
+
|
366
|
+
```ruby
|
367
|
+
GdprAdmin.configure do |config|
|
368
|
+
# Change data policies path to be within the models directory (app/models/gdpr)
|
369
|
+
config.data_policies_path = Rails.root.join('app', 'models', 'gdpr')
|
370
|
+
end
|
371
|
+
```
|
58
372
|
|
59
373
|
## License
|
60
374
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GdprAdmin
|
4
|
+
module DataRetentionPolicyConcern
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
belongs_to :tenant, class_name: GdprAdmin.config.tenant_class
|
9
|
+
|
10
|
+
scope :active, -> { where(active: true) }
|
11
|
+
|
12
|
+
validates :active, inclusion: { in: [true, false] }
|
13
|
+
validates :period_in_days, numericality: { only_integer: true, greater_than: 0 }
|
14
|
+
end
|
15
|
+
|
16
|
+
def process!
|
17
|
+
with_lock do
|
18
|
+
raise InvalidStatusError unless active?
|
19
|
+
return unless should_process?
|
20
|
+
|
21
|
+
Request.create!(
|
22
|
+
tenant: tenant,
|
23
|
+
requester: nil,
|
24
|
+
request_type: :erase_data,
|
25
|
+
data_older_than: period_in_days.days.ago,
|
26
|
+
)
|
27
|
+
update!(last_run_at: Time.now.utc)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def should_process?
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -3,7 +3,14 @@
|
|
3
3
|
module GdprAdmin
|
4
4
|
class Request < ApplicationRecord
|
5
5
|
belongs_to :tenant, class_name: GdprAdmin.config.tenant_class
|
6
|
-
belongs_to :requester, class_name: GdprAdmin.config.requester_class
|
6
|
+
belongs_to :requester, class_name: GdprAdmin.config.requester_class, optional: true
|
7
|
+
|
8
|
+
VALID_STATUS_TRANSITIONS = {
|
9
|
+
pending: %i[processing],
|
10
|
+
processing: %i[completed failed],
|
11
|
+
completed: [],
|
12
|
+
failed: %i[pending],
|
13
|
+
}.freeze
|
7
14
|
|
8
15
|
enum status: {
|
9
16
|
pending: 'pending',
|
@@ -13,15 +20,17 @@ module GdprAdmin
|
|
13
20
|
}
|
14
21
|
|
15
22
|
enum request_type: {
|
16
|
-
|
17
|
-
|
23
|
+
export_subject: 'export_subject',
|
24
|
+
erase_data: 'erase_data',
|
18
25
|
erase_subject: 'erase_subject',
|
19
|
-
erase_timeframe: 'erase_timeframe',
|
20
26
|
}
|
21
27
|
|
22
28
|
before_validation :set_default_data_older_than!
|
23
29
|
after_create_commit :schedule_processing
|
24
30
|
|
31
|
+
validates :status, presence: true
|
32
|
+
validate :valid_status_transition?
|
33
|
+
|
25
34
|
def process!
|
26
35
|
GdprAdmin.load_data_policies
|
27
36
|
with_lock { processing! }
|
@@ -30,24 +39,28 @@ module GdprAdmin
|
|
30
39
|
completed!
|
31
40
|
end
|
32
41
|
rescue StandardError
|
33
|
-
failed!
|
42
|
+
with_lock { failed! }
|
34
43
|
raise
|
35
44
|
end
|
36
45
|
|
37
46
|
def erase?
|
38
|
-
|
47
|
+
erase_data? || erase_subject?
|
39
48
|
end
|
40
49
|
|
41
50
|
def export?
|
42
|
-
|
51
|
+
export_subject?
|
43
52
|
end
|
44
53
|
|
45
54
|
def schedule_processing
|
46
|
-
RequestProcessorJob.set(wait:
|
55
|
+
RequestProcessorJob.set(wait: grace_period).perform_later(self)
|
47
56
|
end
|
48
57
|
|
49
58
|
private
|
50
59
|
|
60
|
+
def grace_period
|
61
|
+
export? ? GdprAdmin.config.export_grace_period : GdprAdmin.config.erasure_grace_period
|
62
|
+
end
|
63
|
+
|
51
64
|
def process_policies
|
52
65
|
ApplicationDataPolicy.descendants.each do |policy_class|
|
53
66
|
policy = policy_class.new(self)
|
@@ -63,5 +76,13 @@ module GdprAdmin
|
|
63
76
|
def set_default_data_older_than!
|
64
77
|
self.data_older_than ||= Time.zone.now
|
65
78
|
end
|
79
|
+
|
80
|
+
def valid_status_transition?
|
81
|
+
return true if status_was.nil? || status.nil? || status_was == status
|
82
|
+
return true if VALID_STATUS_TRANSITIONS[status_was.to_sym].include?(status.to_sym)
|
83
|
+
|
84
|
+
errors.add(:status, :invalid_transition)
|
85
|
+
false
|
86
|
+
end
|
66
87
|
end
|
67
88
|
end
|
@@ -4,7 +4,7 @@ class CreateGdprAdminRequests < ActiveRecord::Migration[7.0]
|
|
4
4
|
def change
|
5
5
|
create_table :gdpr_admin_requests do |t|
|
6
6
|
t.references :tenant, null: false, foreign_key: { to_table: :organizations }
|
7
|
-
t.references :requester, null:
|
7
|
+
t.references :requester, null: true, foreign_key: { to_table: :admin_users }
|
8
8
|
t.string :request_type, null: false
|
9
9
|
t.string :status, default: 'pending', null: false
|
10
10
|
t.datetime :data_older_than, null: false
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateGdprAdminDataRetentionPolicies < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :gdpr_admin_data_retention_policies do |t|
|
6
|
+
t.references :tenant, null: false, foreign_key: { to_table: :organizations }, index: { unique: true }
|
7
|
+
t.integer :period_in_days, null: false
|
8
|
+
t.boolean :active, null: false, default: false
|
9
|
+
t.datetime :last_run_at
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -6,6 +6,34 @@ module GdprAdmin
|
|
6
6
|
def anonymize_phone_number
|
7
7
|
Faker::PhoneNumber.phone_number
|
8
8
|
end
|
9
|
+
|
10
|
+
def anonymize_street_address
|
11
|
+
Faker::Address.street_address
|
12
|
+
end
|
13
|
+
|
14
|
+
def anonymize_city
|
15
|
+
Faker::Address.city
|
16
|
+
end
|
17
|
+
|
18
|
+
def anonymize_state
|
19
|
+
Faker::Address.state
|
20
|
+
end
|
21
|
+
|
22
|
+
def anonymize_zip
|
23
|
+
Faker::Address.zip
|
24
|
+
end
|
25
|
+
|
26
|
+
def anonymize_country
|
27
|
+
Faker::Address.country
|
28
|
+
end
|
29
|
+
|
30
|
+
def anonymize_country_code2
|
31
|
+
Faker::Address.country_code
|
32
|
+
end
|
33
|
+
|
34
|
+
def anonymize_country_code3
|
35
|
+
Faker::Address.country_code_long
|
36
|
+
end
|
9
37
|
end
|
10
38
|
end
|
11
39
|
end
|
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
module GdprAdmin
|
4
4
|
class Configuration
|
5
|
-
attr_accessor :tenant_class, :requester_class, :data_policies_path
|
5
|
+
attr_accessor :tenant_class, :requester_class, :data_policies_path, :default_job_queue,
|
6
|
+
:erasure_grace_period, :export_grace_period
|
6
7
|
attr_writer :tenant_adapter
|
7
8
|
|
8
9
|
def initialize
|
@@ -10,6 +11,9 @@ module GdprAdmin
|
|
10
11
|
@requester_class = 'AdminUser'
|
11
12
|
@tenant_adapter = TenantAdapters::ActsAsTenantAdapter.new
|
12
13
|
@data_policies_path = Rails.root.join('app', 'gdpr')
|
14
|
+
@default_job_queue = :default
|
15
|
+
@erasure_grace_period = 4.hours
|
16
|
+
@export_grace_period = nil
|
13
17
|
end
|
14
18
|
|
15
19
|
def tenant_adapter
|
@@ -1,15 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
5
|
-
require_relative '../anonymizers/internet_anonymizer'
|
3
|
+
require_relative './paper_trail_helper'
|
4
|
+
require_relative './field_anonymizer_helper'
|
6
5
|
|
7
6
|
module GdprAdmin
|
8
7
|
module Helpers
|
9
8
|
module EraseHelper
|
10
|
-
include
|
11
|
-
include
|
12
|
-
include Anonymizers::InternetAnonymizer
|
9
|
+
include FieldAnonymizerHelper
|
10
|
+
include PaperTrailHelper
|
13
11
|
|
14
12
|
##
|
15
13
|
# Erases the given fields on the given record using the method given.
|
@@ -23,7 +21,7 @@ module GdprAdmin
|
|
23
21
|
# { field: :email, method: :anonymize_email },
|
24
22
|
# { field: :password_digest, method: :anonymize_password },
|
25
23
|
# { field: :phone_number, method: ->(record) { record.phone_number.split('').shuffle.join } },
|
26
|
-
# { field: :address, method: :
|
24
|
+
# { field: :address, method: :nilify },
|
27
25
|
# ]
|
28
26
|
# ```
|
29
27
|
#
|
@@ -47,35 +45,11 @@ module GdprAdmin
|
|
47
45
|
# @param fields [Array<Hash>] The fields to erase
|
48
46
|
# @param base_fields [Hash] The fields to update on the record together with the erased fields
|
49
47
|
def erase_fields(record, fields, base_fields = {})
|
50
|
-
new_data = fields.inject(base_fields) do |
|
51
|
-
|
52
|
-
|
53
|
-
res.merge(
|
54
|
-
curr[:field] => call_method(curr[:method], record, record[curr[:seed] || curr[:field]]),
|
55
|
-
)
|
48
|
+
new_data = fields.inject(base_fields) do |changes, curr|
|
49
|
+
changes.merge(curr[:field] => anonymize_field(record, curr))
|
56
50
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
def nillify
|
61
|
-
nil
|
62
|
-
end
|
63
|
-
|
64
|
-
def with_seed(seed)
|
65
|
-
Faker::Config.random = Random.new(seed.to_s.chars.sum(&:ord)) if defined?(Faker)
|
66
|
-
yield
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
def call_method(erase_method, record, seed)
|
72
|
-
return if erase_method.nil?
|
73
|
-
|
74
|
-
erase_method = method(erase_method) if erase_method.is_a?(Symbol)
|
75
|
-
|
76
|
-
arity = erase_method.arity
|
77
|
-
with_seed(seed) do
|
78
|
-
erase_method.call(*[record, seed].take(arity))
|
51
|
+
without_paper_trail do
|
52
|
+
record.update_columns(new_data)
|
79
53
|
end
|
80
54
|
end
|
81
55
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../anonymizers/name_anonymizer'
|
4
|
+
require_relative '../anonymizers/company_anonymizer'
|
5
|
+
require_relative '../anonymizers/contact_anonymizer'
|
6
|
+
require_relative '../anonymizers/internet_anonymizer'
|
7
|
+
|
8
|
+
module GdprAdmin
|
9
|
+
module Helpers
|
10
|
+
module FieldAnonymizerHelper
|
11
|
+
include Anonymizers::NameAnonymizer
|
12
|
+
include Anonymizers::CompanyAnonymizer
|
13
|
+
include Anonymizers::ContactAnonymizer
|
14
|
+
include Anonymizers::InternetAnonymizer
|
15
|
+
|
16
|
+
def anonymize_field(record, field)
|
17
|
+
field_name = field[:field]
|
18
|
+
value = record[field_name]
|
19
|
+
return value if value.nil?
|
20
|
+
|
21
|
+
seed = record[field[:seed] || field_name]
|
22
|
+
anonymize_field_value(record, field.merge(seed: seed))
|
23
|
+
end
|
24
|
+
|
25
|
+
def nilify
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def nullify
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_seed(seed)
|
34
|
+
Faker::Config.random = Random.new(seed.to_s.chars.sum(&:ord)) if defined?(Faker)
|
35
|
+
yield
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def anonymize_field_value(record, field)
|
41
|
+
return field[:seed] if field[:seed].blank?
|
42
|
+
|
43
|
+
call_method(field[:method], record, field[:field], field[:seed])
|
44
|
+
end
|
45
|
+
|
46
|
+
def call_method(erase_method, record, field, seed)
|
47
|
+
raise ArgumentError, "Erase method is not defined for #{field}" if erase_method.nil?
|
48
|
+
return seed if erase_method == :skip
|
49
|
+
|
50
|
+
erase_method = method(erase_method) if erase_method.is_a?(Symbol)
|
51
|
+
|
52
|
+
arity = erase_method.arity
|
53
|
+
with_seed(seed) do
|
54
|
+
erase_method.call(*[record, field, seed].take(arity))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './field_anonymizer_helper'
|
4
|
+
|
5
|
+
module GdprAdmin
|
6
|
+
module Helpers
|
7
|
+
module PaperTrailHelper
|
8
|
+
include FieldAnonymizerHelper
|
9
|
+
|
10
|
+
def without_paper_trail
|
11
|
+
return yield unless defined?(::PaperTrail)
|
12
|
+
|
13
|
+
begin
|
14
|
+
current_status = ::PaperTrail.enabled?
|
15
|
+
::PaperTrail.enabled = false
|
16
|
+
yield
|
17
|
+
ensure
|
18
|
+
::PaperTrail.enabled = current_status
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def anonymize_version_object(version, fields)
|
23
|
+
object = version.object.deep_dup
|
24
|
+
return object if object.blank?
|
25
|
+
return {} if object.is_a?(Array)
|
26
|
+
|
27
|
+
fields.each do |field|
|
28
|
+
field_name = field[:field].to_s
|
29
|
+
field_value = object[field_name]
|
30
|
+
next if field_value.nil?
|
31
|
+
|
32
|
+
new_value = anonymize_field(version.item, field)
|
33
|
+
object.merge!(field_name => new_value)
|
34
|
+
end
|
35
|
+
object
|
36
|
+
end
|
37
|
+
|
38
|
+
def anonymize_version_object_changes(version, fields)
|
39
|
+
return unless version.respond_to?(:object_changes)
|
40
|
+
|
41
|
+
object_changes = version.object_changes.deep_dup
|
42
|
+
return object_changes if object_changes.blank?
|
43
|
+
return {} if object_changes.is_a?(Array)
|
44
|
+
|
45
|
+
fields.each do |field|
|
46
|
+
field_name = field[:field].to_s
|
47
|
+
changes = object_changes[field_name]
|
48
|
+
next if changes.nil?
|
49
|
+
|
50
|
+
new_value = anonymize_object_changes_array(version, changes, field)
|
51
|
+
object_changes.merge!(field_name => new_value)
|
52
|
+
end
|
53
|
+
object_changes
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def anonymize_object_changes_array(version, changes, field)
|
59
|
+
changes.map do |value|
|
60
|
+
anonymize_field_value(version.item, field.merge(seed: value))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GdprAdmin
|
4
|
+
module PaperTrail
|
5
|
+
class VersionDataPolicy < GdprAdmin::ApplicationDataPolicy
|
6
|
+
def scope
|
7
|
+
::PaperTrail::Version.none
|
8
|
+
end
|
9
|
+
|
10
|
+
def erase(version, item_fields = nil)
|
11
|
+
item_fields ||= infer_item_fields(version)
|
12
|
+
return if item_fields.nil?
|
13
|
+
|
14
|
+
base_changes = {
|
15
|
+
object: anonymize_version_object(version, item_fields),
|
16
|
+
object_changes: anonymize_version_object_changes(version, item_fields),
|
17
|
+
}.compact
|
18
|
+
erase_fields(version, fields, base_changes)
|
19
|
+
end
|
20
|
+
|
21
|
+
def fields
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def infer_item_fields(version)
|
28
|
+
infer_data_policy_class(version)&.new(request)&.try(:fields)
|
29
|
+
end
|
30
|
+
|
31
|
+
def infer_data_policy_class(version)
|
32
|
+
model = version.item_type.constantize.new
|
33
|
+
return model.data_policy_class if model.respond_to?(:data_policy_class)
|
34
|
+
|
35
|
+
prefix = model.data_policy_prefix if model.respond_to?(:data_policy_prefix)
|
36
|
+
"#{prefix}#{version.item_type}DataPolicy".constantize
|
37
|
+
rescue NameError
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/gdpr_admin/version.rb
CHANGED
data/lib/gdpr_admin.rb
CHANGED
@@ -3,9 +3,13 @@
|
|
3
3
|
require 'gdpr_admin/version'
|
4
4
|
require 'gdpr_admin/engine'
|
5
5
|
require 'gdpr_admin/configuration'
|
6
|
+
require 'gdpr_admin/error'
|
7
|
+
require 'gdpr_admin/invalid_status_error'
|
6
8
|
require 'gdpr_admin/skip_data_policy_error'
|
7
9
|
require 'gdpr_admin/helpers/erase_helper'
|
10
|
+
require 'gdpr_admin/helpers/scope_helper'
|
8
11
|
require 'gdpr_admin/application_data_policy'
|
12
|
+
require 'gdpr_admin/paper_trail/version_data_policy'
|
9
13
|
require 'gdpr_admin/tenant_adapters/acts_as_tenant_adapter'
|
10
14
|
|
11
15
|
begin
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gdpr_admin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Colex
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-03-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 6.1.7
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faker
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.1.1
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.1.1
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rspec-rails
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -133,19 +147,30 @@ files:
|
|
133
147
|
- README.md
|
134
148
|
- Rakefile
|
135
149
|
- app/jobs/gdpr_admin/application_job.rb
|
150
|
+
- app/jobs/gdpr_admin/data_retention_policies_runner_job.rb
|
136
151
|
- app/jobs/gdpr_admin/request_processor_job.rb
|
152
|
+
- app/models/concerns/gdpr_admin/data_retention_policy_concern.rb
|
137
153
|
- app/models/gdpr_admin/application_record.rb
|
154
|
+
- app/models/gdpr_admin/data_retention_policy.rb
|
138
155
|
- app/models/gdpr_admin/request.rb
|
139
156
|
- config/routes.rb
|
140
157
|
- db/migrate/20230212185817_create_gdpr_admin_requests.rb
|
158
|
+
- db/migrate/20230216171202_create_gdpr_admin_data_retention_policies.rb
|
141
159
|
- lib/gdpr_admin.rb
|
160
|
+
- lib/gdpr_admin/anonymizers/company_anonymizer.rb
|
142
161
|
- lib/gdpr_admin/anonymizers/contact_anonymizer.rb
|
143
162
|
- lib/gdpr_admin/anonymizers/internet_anonymizer.rb
|
144
163
|
- lib/gdpr_admin/anonymizers/name_anonymizer.rb
|
145
164
|
- lib/gdpr_admin/application_data_policy.rb
|
146
165
|
- lib/gdpr_admin/configuration.rb
|
147
166
|
- lib/gdpr_admin/engine.rb
|
167
|
+
- lib/gdpr_admin/error.rb
|
148
168
|
- lib/gdpr_admin/helpers/erase_helper.rb
|
169
|
+
- lib/gdpr_admin/helpers/field_anonymizer_helper.rb
|
170
|
+
- lib/gdpr_admin/helpers/paper_trail_helper.rb
|
171
|
+
- lib/gdpr_admin/helpers/scope_helper.rb
|
172
|
+
- lib/gdpr_admin/invalid_status_error.rb
|
173
|
+
- lib/gdpr_admin/paper_trail/version_data_policy.rb
|
149
174
|
- lib/gdpr_admin/skip_data_policy_error.rb
|
150
175
|
- lib/gdpr_admin/tenant_adapters/acts_as_tenant_adapter.rb
|
151
176
|
- lib/gdpr_admin/version.rb
|