gdpr_admin 1.1.0 → 1.2.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: b019a3a76e13bb0a29a264a2f14aa9979c8a4cf2ef3163014f72f727856d07ca
4
+ data.tar.gz: 735b7ac163d0b9a7579f0bb9c26bde48ca2dc5e00996175a31aaa597dae94597
5
5
  SHA512:
6
- metadata.gz: d3ae19c166459e40ea1d7428883935de10eaae4450d36de7a55e5ad8fd357d0cd8af736423d0823bc08e89645a67c370b660908fd93bb5d29c93bf6a0140ed8e
7
- data.tar.gz: 95e6688226094762e6a25915075d2e26addf4d8f82ec5d799c89d097bdc752c1df1944ea4971eca2dc21f856d527e31f3d308e3b5cafd361e13a5b4d95af7373
6
+ metadata.gz: 14e0c4a02403085c3c1fd5f7b29625f0b8194ab41a374e0f58213aa59f27e5b7e8a4d9477cbb53975fcd331af9d20516cfd3a37ffa6cc6007579ddd72bbba5a7
7
+ data.tar.gz: 63d3e8710805b8f339da69a4e93deb501d6bc7734eb21f542c3ca15683114e6c8b57e73d762a2e77294c9b3522e3eb35e5aa79d960a9bf5d6c6ce68081124c4e
data/README.md CHANGED
@@ -1,6 +1,18 @@
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
+ [![Gem Version](https://badge.fury.io/rb/gdpr_admin.svg)](http://badge.fury.io/rb/gdpr_admin)
10
+
2
11
  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
12
 
13
+ GDPR Admin uses simple Ruby classes, so you're free to code your Data Policies as you see fit. A swiss knife of
14
+ helper methods are available to make the processes even simpler.
15
+
4
16
  ## Installation
5
17
  Add this line to your application's Gemfile:
6
18
 
@@ -48,13 +60,290 @@ Once you have all your data policies defined, create a `GdprAdmin::Request` to p
48
60
  GdprAdmin::Request.create!(
49
61
  tenant: current_tenant,
50
62
  requester: current_admin_user,
51
- request_type: 'erase_all',
63
+ request_type: 'erase_data',
52
64
  data_older_than: 1.month.ago, # Optional: by default, it will be todays date
53
65
  )
54
66
  ```
55
67
 
56
68
  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
69
 
70
+ ### Anonymization Helpers
71
+ A set of helper methods are available to make the anonymization even simpler. These are not mandatory, but can help you
72
+ keep your code cleaner and, better, write less code.
73
+
74
+ #### `erase_fields`
75
+
76
+ ```ruby
77
+ erase_fields(record, fields, base_changes = {})
78
+ ```
79
+
80
+ The method `erase_fields` is available in the Data Policy class. It expects an array of field anonymization options.
81
+ It will automatically process those fields and update the record in the database using `update_columns` (so validations
82
+ are skipped). The last optional argument (`base_changes`) is a hash of attributes that should be updated when the record
83
+ is updated. See the example below:
84
+
85
+ ```ruby
86
+ class ContactDataPolicy < GdprAdmin::ApplicationDataPolicy
87
+ def fields
88
+ [
89
+ { field: :first_name, method: :anonymize_first_name },
90
+ { field: :last_name, method: :anonymize_last_name },
91
+ { field: :gender, method: :skip },
92
+ {
93
+ field: :email,
94
+ method: lambda { |contact|
95
+ domain = contact.email[/@.*/]
96
+ "anonymous.contact#{contact.id}#{domain}"
97
+ },
98
+ },
99
+ { field: :street_address, method: :nilify },
100
+ { field: :city, method: :anonymize_city, seed: :id },
101
+ ]
102
+ end
103
+
104
+ def erase(contact)
105
+ erase_fields(contact, fields, { anonymized_at: Time.zone.now })
106
+ end
107
+ end
108
+ ```
109
+
110
+ The anonymizers used above (e.g. `anonymize_first_name`), by default, will use the value of the field being updated as
111
+ the seed. That means that, when anonymization with the same anonymizer function, equal values will **always** yield the
112
+ same anonymized value. _(note: different values may also yield the same value)_
113
+
114
+ To use the built-in anonymizer functions, you need to install the gem `faker`.
115
+
116
+ ## GDPR Request
117
+ A GDPR Request (`GdprAdmin::Request`) represents a request to remove a subject's data, tenant's data, or export subject data.
118
+
119
+ ### Model `GdprAdmin::Request`
120
+
121
+ ```ruby
122
+ GdprAdmin::Request.create!(
123
+ tenant: current_tenant,
124
+ requester: current_admin_user,
125
+ request_type: 'erase_data',
126
+ data_older_than: 30.days.ago,
127
+ )
128
+ ```
129
+
130
+ #### `#tenant`
131
+ Defines which tenant is this request attributed to. All requests must be assigned to a Tenant.
132
+
133
+ #### `#request_type`
134
+ Represents what type of request is being performed. Must be one of:
135
+ - `erase_data`: erase a tenant's data;
136
+ - `erase_subject`: erase a single subject's data in a tenant;
137
+ - `export_subject`: export data about a subject within a tenant;
138
+
139
+ #### `#data_older_than`
140
+ The request should only affect any data created before than date defined here (e.g. erase anything older than 30 days ago).
141
+
142
+ #### `#process!`
143
+ This method executes the request. This is automatically called by the `GdprAdmin::RequestProcessorJob`.
144
+
145
+ ## Data Retention Policies
146
+ Some tenants may have different data retention policies (e.g. some hold Personally identifiable information for 3 months,
147
+ others for 1 month).
148
+
149
+ ### Setup Job
150
+ Run the job `GdprAdmin::DataRetentionPoliciesRunnerJob` periodically to execute your data retention policy (it is recommended
151
+ to run it, at least, once a day).
152
+
153
+ #### Sidekiq
154
+ You can define a repeating job with `sidekiq-cron`:
155
+
156
+ ```ruby
157
+ Sidekiq::Cron::Job.create(
158
+ class: SyncOrganizationsContactsJob.to_s,
159
+ cron: '0 2 * * *',
160
+ name: 'Sync Organization Contacts',
161
+ queue: 'cron',
162
+ active_job: true,
163
+ )
164
+ ```
165
+
166
+ #### DelayedJobs
167
+ ```ruby
168
+ GdprAdmin::DataRetentionPoliciesRunnerJob.set(cron: '0 2 * * *', queue: :cron).perform_later
169
+ ```
170
+
171
+ ### Model `GdprAdmin::DataRetentionPolicy`
172
+
173
+ This model allows you to create a custom data policy for each tenant and automatically run the policy periodically.
174
+
175
+ ```ruby
176
+ GdprAdmin::DataRetentionPolicy.create!(
177
+ tenant: acme_inc,
178
+ period_in_days: 30,
179
+ )
180
+ ```
181
+
182
+ If you want to have a custom logic for running the data retention policy (e.g. do not run if the organization is deleted),
183
+ then you can add the logic to the method `#should_process?`:
184
+
185
+ ```ruby
186
+ module GdprAdmin
187
+ class DataRetentionPolicy < GdprAdmin::ApplicationRecord
188
+ include GdprAdmin::DataRetentionPolicyConcern
189
+
190
+ def should_process?
191
+ tenant.deleted_at.nil?
192
+ end
193
+ end
194
+ end
195
+ ```
196
+
197
+
198
+ ## PaperTrail
199
+ GDPR Admin provides a set of tools to keep your PaperTrail GDPR Compliant.
200
+
201
+ ### PaperTrail Data Privacy
202
+ By default, PaperTrail versions will not be anonymized. You may extend the default `PaperTrail::VersionDataPrivacy`
203
+ with your own scope. If you track custom fields with your versions (e.g. `ip`), then you can also define an anonymizer
204
+ for those here:
205
+
206
+ ```ruby
207
+ # app/gdpr/paper_trail/version_data_privacy.rb
208
+
209
+ module PaperTrail
210
+ class VersionDataPolicy < GdprAdmin::PaperTrail::VersionDataPolicy
211
+ def fields
212
+ [
213
+ { field: 'ip', method: :anonymize_ip },
214
+ ]
215
+ end
216
+
217
+ def scope
218
+ return PaperTrail::Version.where(updated_at: ...request.data_older_than) if request.erase_data?
219
+
220
+ PaperTrail::Version.none
221
+ end
222
+ end
223
+ end
224
+ ```
225
+
226
+ #### `PaperTrail::VersionDataPolicy#erase`
227
+ **NOTE:** this method only support JSON format for `object` and, optionally, `object_changes`. If you need a different
228
+ format, you will need to re-implement this method as desired.
229
+
230
+ `erase(version, item_fields = nil)`
231
+
232
+ The `erase` method will, by default, anonymize the data within `object` and `object_changes` (and whichever fields are
233
+ defined in the `#fields` method). It will choose which fields to anonymize the `object` and `object_changes` and which
234
+ anonymization methods by finding the `item`'s data policy and loading the fields from its `fields` method. Unless
235
+ `item_fields` is defined, in which case it will be used instead.
236
+
237
+ For example, if you have a `item_type` set to `User`, it will try to find the `UserDataPrivacy`. If you want to use a
238
+ different class for a `item_type`, you must define a `data_policy_class` in the model.
239
+
240
+ ```ruby
241
+ class User < ApplicationRecord
242
+ def data_policy_class
243
+ PersonDataPolicy
244
+ end
245
+ end
246
+ ```
247
+
248
+ If you'd like to just namespace all policies, then you can define `data_policy_prefix` in the `ApplicationRecord`:
249
+
250
+ ```ruby
251
+ class ApplicationRecord < ActiveRecord::Base
252
+ def data_policy_prefix
253
+ 'Gdpr::'
254
+ end
255
+ end
256
+
257
+ # Now, user should be defined in `Gdpr::UserDataPolicy`
258
+ ```
259
+
260
+ ### PaperTrail Helpers
261
+ When using the method `erase_fields`, no PaperTrail versions will be created in the database. GDPR Admin offer other
262
+ helper methods to deal with PaperTrail. (If you are not using `paper_trail`, this section may not be relevant)
263
+
264
+ #### `without_paper_trail`
265
+
266
+ Given a block, this method will execute it in a context where PaperTrail is disabled, so no versions are created:
267
+ ```ruby
268
+ def erase(contact)
269
+ without_paper_trail do
270
+ contact.update!(first_name: 'John', last_name: 'Doe')
271
+ end
272
+ end
273
+ ```
274
+
275
+ As mentioned above, this is **not** required when using `erase_fields` as it is the default behavior.
276
+
277
+ ## Configuration
278
+ Configure GDPR Admin in a initializer file `config/initializers/gdpr_admin.rb`. The configuration should be done within
279
+ the block of `GdprAdmin.configure(&block)`:
280
+
281
+ ```ruby
282
+ # config/initializers/gdpr_admin/rb
283
+ GdprAdmin.configure do |config|
284
+ # GDPR Admin configuration here...
285
+ end
286
+ ```
287
+
288
+ ### Multi-Tenancy
289
+ GDPR Admin is built maily for B2B SaaS services and it assumes that the service may have multiple tenants. The
290
+ `GDPR::Request` object always expects a `tenant` to be provided. When processing the request, data will be automatically
291
+ segregated using the tenant adapter.
292
+
293
+ #### Tenant Class
294
+ By default, GDPR Admin will assume that the tenant class is `Organization`. You can change that by setting the
295
+ `tenant_class` in the config.
296
+
297
+ ```ruby
298
+ GdprAdmin.configure do |config|
299
+ config.tenant_class = 'Tenant'
300
+ end
301
+ ```
302
+
303
+ #### ActsAsTenant Adapter
304
+ You can segregated the process to a tenant using `ActsAsTenant` gem:
305
+
306
+ ```ruby
307
+ GdprAdmin.configure do |config|
308
+ config.tenant_adapter = :acts_as_tenant
309
+ end
310
+ ```
311
+
312
+ ### Jobs
313
+ Requests are processed asynchronously using ActiveJob.
314
+
315
+ #### Custom Queue
316
+ You can set which queue should be used to schedule the job for processing the request using the config `default_job_queue`:
317
+
318
+ ```ruby
319
+ GdprAdmin.configure do |config|
320
+ config.default_job_queue = :gdpr_tasks
321
+ end
322
+ ```
323
+
324
+ #### Grace Periods
325
+ To allow for cancelling any accidental erasure request, the jobs are scheduled with a configurable grace period.
326
+ By default, erasure requests will wait 4 hours before being executed, while export requests will be executed immediately.
327
+
328
+ ```ruby
329
+ GdprAdmin.configure do |config|
330
+ config.erasure_grace_period = 1.day
331
+ config.export_grace_period = 2.minutes
332
+ end
333
+ ```
334
+
335
+ ### Other Configurations
336
+
337
+ #### Data Policies Directory
338
+ By default, GDPR Admin will assume that your data policies are defined in `app/gdpr`. If you wish to have them
339
+ in a different place, you can change the option `data_policies_path`:
340
+
341
+ ```ruby
342
+ GdprAdmin.configure do |config|
343
+ # Change data policies path to be within the models directory (app/models/gdpr)
344
+ config.data_policies_path = Rails.root.join('app', 'models', 'gdpr')
345
+ end
346
+ ```
58
347
 
59
348
  ## License
60
349
  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
@@ -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,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.2.0'
5
6
  end
7
+ # :nocov:
data/lib/gdpr_admin.rb CHANGED
@@ -3,9 +3,12 @@
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'
8
10
  require 'gdpr_admin/application_data_policy'
11
+ require 'gdpr_admin/paper_trail/version_data_policy'
9
12
  require 'gdpr_admin/tenant_adapters/acts_as_tenant_adapter'
10
13
 
11
14
  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.2.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-02-16 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,29 @@ 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/invalid_status_error.rb
172
+ - lib/gdpr_admin/paper_trail/version_data_policy.rb
149
173
  - lib/gdpr_admin/skip_data_policy_error.rb
150
174
  - lib/gdpr_admin/tenant_adapters/acts_as_tenant_adapter.rb
151
175
  - lib/gdpr_admin/version.rb