gdpr_admin 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 805cbc1723602328c1f50a94bc0ce0ec9bf3e06aa68bfc8ea4ed4b209cd16bca
4
- data.tar.gz: b14d9f47e9f06d9ef5480dd42ab9fa3c287e9c2c1a1241c997ba75f6a5e18e17
3
+ metadata.gz: 9b5cc14d1e0f88e99bec3ccd100795cc1985ef86fa559fbdd2639bdd5bef1ee4
4
+ data.tar.gz: 5c07836dec53a6265e3fcd6516f910a22b6834234bea5f39890030ba394d0e5c
5
5
  SHA512:
6
- metadata.gz: d3ae19c166459e40ea1d7428883935de10eaae4450d36de7a55e5ad8fd357d0cd8af736423d0823bc08e89645a67c370b660908fd93bb5d29c93bf6a0140ed8e
7
- data.tar.gz: 95e6688226094762e6a25915075d2e26addf4d8f82ec5d799c89d097bdc752c1df1944ea4971eca2dc21f856d527e31f3d308e3b5cafd361e13a5b4d95af7373
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
+ [![Build Status](https://github.com/Colex/gdpr_admin/actions/workflows/build.yml/badge.svg)](https://github.com/Colex/gdpr_admin/actions/workflows/build.yml)
8
+ [![Code Climate](https://codeclimate.com/github/Colex/gdpr_admin.svg)](https://codeclimate.com/github/Colex/gdpr_admin)
9
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/00740133ef1f97181ed6/test_coverage)](https://codeclimate.com/github/Colex/gdpr_admin/test_coverage)
10
+ [![Gem Version](https://badge.fury.io/rb/gdpr_admin.svg)](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: 'erase_all',
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GdprAdmin
4
+ class DataRetentionPoliciesRunnerJob < ApplicationJob
5
+ queue_as GdprAdmin.config.default_job_queue
6
+
7
+ def perform
8
+ GdprAdmin::DataRetentionPolicy.active.find_each(&:process!)
9
+ end
10
+ end
11
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module GdprAdmin
4
4
  class RequestProcessorJob < ApplicationJob
5
+ queue_as GdprAdmin.config.default_job_queue
6
+
5
7
  def perform(task)
6
8
  task.process!
7
9
  end
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GdprAdmin
4
+ class DataRetentionPolicy < ApplicationRecord
5
+ include GdprAdmin::DataRetentionPolicyConcern
6
+ end
7
+ 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
- subject_export: 'subject_export',
17
- erase_all: 'erase_all',
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
- erase_all? || erase_subject? || erase_timeframe?
47
+ erase_data? || erase_subject?
39
48
  end
40
49
 
41
50
  def export?
42
- subject_export?
51
+ export_subject?
43
52
  end
44
53
 
45
54
  def schedule_processing
46
- RequestProcessorJob.set(wait: 4.hours).perform_later(self)
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: false, foreign_key: { to_table: :admin_users }
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GdprAdmin
4
+ module Anonymizers
5
+ module CompanyAnonymizer
6
+ def anonymize_company
7
+ Faker::Company.name
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -10,6 +10,10 @@ module GdprAdmin
10
10
  def anonymize_password(record)
11
11
  record.send(:password_digest, SecureRandom.hex(32))
12
12
  end
13
+
14
+ def anonymize_ip
15
+ Faker::Internet.ip_v4_address
16
+ end
13
17
  end
14
18
  end
15
19
  end
@@ -3,6 +3,7 @@
3
3
  module GdprAdmin
4
4
  class ApplicationDataPolicy
5
5
  include Helpers::EraseHelper
6
+ include Helpers::ScopeHelper
6
7
 
7
8
  def initialize(request)
8
9
  @request = request
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GdprAdmin
4
+ class Error < StandardError; end
5
+ end
@@ -1,15 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../anonymizers/name_anonymizer'
4
- require_relative '../anonymizers/contact_anonymizer'
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 Anonymizers::NameAnonymizer
11
- include Anonymizers::ContactAnonymizer
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: :nillify },
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 |res, curr|
51
- next res if record[curr[:field]].nil?
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
- record.update_columns(new_data)
58
- end
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GdprAdmin
4
+ module Helpers
5
+ module ScopeHelper
6
+ def scope_by_date(scope, field = :updated_at)
7
+ scope.where(field => ...request.data_older_than)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GdprAdmin
4
+ class InvalidStatusError < Error; end
5
+ 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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :nocov:
3
4
  module GdprAdmin
4
- VERSION = '1.1.0'
5
+ VERSION = '1.3.0'
5
6
  end
7
+ # :nocov:
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.1.0
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-02-14 00:00:00.000000000 Z
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