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 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