anony 1.1.0 → 1.2.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: 30ab02373cbc28f7d884d0fdfa88da77ec4a1628056332363d9899f7627e629d
4
- data.tar.gz: bfbad23ad3325b0641ef5cfb2cfb21859f0db5b2a388982342bc43e74df722ac
3
+ metadata.gz: ebdd2184f519951dc6fef205b2e55c0b1eaadc50849f91e17087521e6ec3d887
4
+ data.tar.gz: 8be91d24763bf780ba745cd0c4a4d9f9283637b79a2a3b32c4e0476366a675ce
5
5
  SHA512:
6
- metadata.gz: f10ce2f8c110fae434df6c37c6946b99715f25b2790ab0da3f8784a991e88cf5f7927800099d23e1b6b5a38f55439bd97d2078382b88139213b943e38515015b
7
- data.tar.gz: 716e44ce317a2712c81e038e0757cca8e99db144a7c8612830fe0af413153859593ca7a45d5c0ce7b55c3f2117ee6fc8bbcc9ee84ffa8e69a4bcbbc61990ffaa
6
+ metadata.gz: 57a6da624bb0c53df166d32b530cbae6fb17966091a72105424513044e6fcabdf67179a56d494e7d242b836559edd5a0384f13a20544e43f564e27fa9a437fd8
7
+ data.tar.gz: 51533727cb125403cc1ee87fb95ea23b8fcaff4cf73a1f5fa67a071805e299ff6924f9093e0458037805fd8fc219221091ac7657b4b85e81dd7d95ba079a9d72
@@ -0,0 +1,7 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ open-pull-requests-limit: 10
@@ -0,0 +1,37 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+
6
+ jobs:
7
+ rubocop:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v3
11
+ - name: Set up Ruby
12
+ uses: ruby/setup-ruby@v1
13
+ with:
14
+ bundler-cache: true
15
+ - name: Run rubocop
16
+ run: bundle exec rubocop --parallel --extra-details --display-style-guide
17
+
18
+ tests:
19
+ strategy:
20
+ matrix:
21
+ ruby-version: [2.6, 2.7, 3.0, 3.1]
22
+ rails-version: [6.0, 6.1, 7.0]
23
+ exclude:
24
+ - ruby-version: 2.6
25
+ rails-version: 7.0
26
+ runs-on: ubuntu-latest
27
+ env:
28
+ RAILS_VERSION: ${{ matrix.rails-version }}
29
+ steps:
30
+ - uses: actions/checkout@v3
31
+ - name: Set up Ruby
32
+ uses: ruby/setup-ruby@v1
33
+ with:
34
+ bundler-cache: true
35
+ ruby-version: "${{ matrix.ruby-version }}"
36
+ - name: Run tests
37
+ run: bundle exec rspec --format RSpec::Github::Formatter
data/.rubocop.yml CHANGED
@@ -4,9 +4,16 @@ inherit_gem:
4
4
 
5
5
  AllCops:
6
6
  TargetRubyVersion: 3.0
7
+ NewCops: enable
7
8
 
8
9
  Layout/LineLength:
9
10
  Max: 100
10
11
 
12
+ Gemspec/DevelopmentDependencies:
13
+ EnforcedStyle: gemspec
14
+
11
15
  Gemspec/RequiredRubyVersion:
12
16
  Enabled: false
17
+
18
+ RSpec/MultipleExpectations:
19
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # v1.2.0
2
+
3
+ - Add support for configuring multiple superclasses for the `DefineDeletionStrategy` cop [#98](https://github.com/gocardless/anony/pull/98)
4
+ - Introduce helpers (selectors) for anonymising all a subject's records [#97](https://github.com/gocardless/anony/pull/97)
5
+
1
6
  # v1.1.0
2
7
 
3
8
  - Drop ruby 2.4 and 2.5 support
data/README.md CHANGED
@@ -243,6 +243,42 @@ irb(main):003:0> manager
243
243
  => #<Manager first_name="e9ab2800-d4b9-4227-94a7-7f81118d8a8a" last_name="previous-name-of-42">
244
244
  ```
245
245
 
246
+ ### Anonymising many records, or anonymising by subject
247
+
248
+ **Note**: This is an experimental feature and has not been tested widely
249
+ in production environments.
250
+
251
+ You can use selectors to anonymise multiple records. You first define a block for
252
+ a specific subject that returns a list of anonymisable records.
253
+
254
+ ```ruby
255
+ anonymise do
256
+ selectors do
257
+ for_subject(:user_id) { |user_id| find_all_users(user_id) }
258
+ end
259
+ end
260
+ ```
261
+
262
+ You can also use `scopes`, `where`, etc when defining your selectors:
263
+
264
+ ```ruby
265
+ anonymise do
266
+ selectors do
267
+ for_subject(:user_id) { |user_id| where(user_id: user_id) }
268
+ end
269
+ end
270
+ ```
271
+
272
+ This can then be used to anonymise all those subject using this API:
273
+
274
+ ```ruby
275
+ ModelName.anonymise_for!(:user_id, "user_1234")
276
+ ```
277
+
278
+ If you attempt to anonymise records with a selector that has not been defined it
279
+ will throw an error.
280
+
281
+
246
282
  ### Identifying anonymised records
247
283
 
248
284
  If your model has an `anonymised_at` column, Anony will automatically set that value
@@ -281,6 +317,12 @@ class Employees < ApplicationRecord
281
317
  end
282
318
  ```
283
319
 
320
+ There is also a helper defined when `Anony::Anonymisable" is included:
321
+
322
+ ```ruby
323
+ Employees.anonymised?
324
+ ```
325
+
284
326
  ### Preventing anonymisation
285
327
 
286
328
  You might have a need to preserve model data in some (or all) circumstances. Anony exposes
@@ -365,6 +407,7 @@ Anony::Config.ignore_fields(:id, :created_at, :updated_at)
365
407
  By default, `Config.ignore_fields` is an empty array and all fields are considered
366
408
  anonymisable.
367
409
 
410
+
368
411
  ## Testing
369
412
 
370
413
  This library ships with a set of useful RSpec examples for your specs. Just require them
@@ -446,6 +489,15 @@ Lint/DefineDeletionStrategy:
446
489
  ModelSuperclass: Acme::Record
447
490
  ```
448
491
 
492
+ If your models use multiple superclasses, you can specify a list of superclasses in your `.rubocop.yml`. Note that you will have to specify `ApplicationRecord` explicitly in this list should you want to lint all models which inherit from `ApplicationRecord`.
493
+ ```yml
494
+ Lint/DefineDeletionStrategy:
495
+ ModelSuperclass:
496
+ - Acme::Record
497
+ - UmbrellaCorp::Record
498
+
499
+ ```
500
+
449
501
  ## License & Contributing
450
502
 
451
503
  * Anony is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/anony.gemspec CHANGED
@@ -24,15 +24,16 @@ Gem::Specification.new do |spec|
24
24
 
25
25
  spec.required_ruby_version = ">= 2.6"
26
26
 
27
- spec.add_development_dependency "bundler", "~> 2.2.0"
28
- spec.add_development_dependency "gc_ruboconfig", "~> 2.24.0"
27
+ spec.add_development_dependency "bundler", "~> 2"
28
+ spec.add_development_dependency "gc_ruboconfig", "~> 3.6.0"
29
29
  spec.add_development_dependency "rspec", "~> 3.9"
30
- spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
30
+ spec.add_development_dependency "rspec-github", "~> 2.4.0"
31
31
  spec.add_development_dependency "yard", "~> 0.9.20"
32
32
 
33
33
  # For integration testing
34
- spec.add_development_dependency "sqlite3", "~> 1.4.1"
34
+ spec.add_development_dependency "sqlite3", "~> 1.6.1"
35
35
 
36
36
  spec.add_dependency "activerecord", ">= 5.2", "< 8"
37
37
  spec.add_dependency "activesupport", ">= 5.2", "< 8"
38
+ spec.metadata["rubygems_mfa_required"] = "true"
38
39
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  require "active_support/core_ext/module/delegation"
4
4
 
5
- require_relative "./strategies/overwrite"
5
+ require_relative "not_anonymisable_exception"
6
+ require_relative "strategies/overwrite"
6
7
  require_relative "model_config"
7
8
 
8
9
  module Anony
@@ -54,6 +55,30 @@ module Anony
54
55
  @anonymise_config.valid?
55
56
  end
56
57
 
58
+ # Finds the records that relate to a particular subject and runs anonymise on
59
+ # each of them. If a selector is not defined it will raise an exception.
60
+ def anonymise_for!(subject, subject_id)
61
+ records = anonymise_config.
62
+ select(subject, subject_id)
63
+ records.map do |record|
64
+ if !record.respond_to?(:anonymise!)
65
+ raise NotAnonymisableException, record
66
+ end
67
+
68
+ record.anonymise!
69
+ end
70
+ end
71
+
72
+ # Checks if a selector has been defined for a given subject.
73
+ # This is useful for when writing tests to check all models have a valid selector
74
+ # for a given subject.
75
+ # @return [Boolean]
76
+ # @example
77
+ # Manager.selector_for?(:user_id)
78
+ def selector_for?(subject)
79
+ anonymise_config.selector_for?(subject)
80
+ end
81
+
57
82
  attr_reader :anonymise_config
58
83
  end
59
84
 
@@ -74,6 +99,10 @@ module Anony
74
99
  Result.failed(e)
75
100
  end
76
101
 
102
+ def anonymised?
103
+ anonymised_at.present?
104
+ end
105
+
77
106
  # @!visibility private
78
107
  def self.included(base)
79
108
  base.extend(ClassMethods)
@@ -36,9 +36,8 @@ module RuboCop
36
36
  end
37
37
 
38
38
  def model?(node)
39
- return unless (superclass = node.children[1])
40
-
41
- superclass.const_name == model_superclass_name
39
+ superclass = node.children[1]
40
+ model_superclass_name.include? superclass&.const_name
42
41
  end
43
42
 
44
43
  def class_name(node)
@@ -46,7 +45,15 @@ module RuboCop
46
45
  end
47
46
 
48
47
  def model_superclass_name
49
- cop_config["ModelSuperclass"] || "ApplicationRecord"
48
+ unless cop_config["ModelSuperclass"]
49
+ return ["ApplicationRecord"]
50
+ end
51
+
52
+ if cop_config["ModelSuperclass"].is_a?(Array)
53
+ return cop_config["ModelSuperclass"]
54
+ end
55
+
56
+ [cop_config["ModelSuperclass"]]
50
57
  end
51
58
  end
52
59
  end
@@ -147,7 +147,7 @@ module Anony
147
147
  # end
148
148
  OverwriteHex = Struct.new(:max_length) do
149
149
  def call(_existing_value)
150
- hex_length = max_length / 2 + 1
150
+ hex_length = (max_length / 2) + 1
151
151
  SecureRandom.hex(hex_length)[0, max_length]
152
152
  end
153
153
  end
@@ -2,8 +2,9 @@
2
2
 
3
3
  require "active_support/core_ext/module/delegation"
4
4
 
5
- require_relative "./strategies/destroy"
6
- require_relative "./strategies/overwrite"
5
+ require_relative "strategies/destroy"
6
+ require_relative "strategies/overwrite"
7
+ require_relative "selectors"
7
8
 
8
9
  module Anony
9
10
  class ModelConfig
@@ -29,6 +30,7 @@ module Anony
29
30
  def initialize(model_class, &block)
30
31
  @model_class = model_class
31
32
  @strategy = UndefinedStrategy.new
33
+ @selectors_config = nil
32
34
  @skip_filter = nil
33
35
  instance_exec(&block) if block
34
36
  end
@@ -45,6 +47,7 @@ module Anony
45
47
  end
46
48
 
47
49
  delegate :valid?, :validate!, to: :@strategy
50
+ delegate :select, to: :@selectors_config
48
51
 
49
52
  # Use the deletion strategy instead of anonymising individual fields. This method is
50
53
  # incompatible with the fields strategy.
@@ -86,6 +89,30 @@ module Anony
86
89
  @strategy = Strategies::Overwrite.new(@model_class, &block)
87
90
  end
88
91
 
92
+ # Define selectors to select records that apply to a particular subject.
93
+ # This method taks a configuration block that then builds Selectors
94
+ #
95
+ # @see Anony::Selectors
96
+ #
97
+ # @example
98
+ #
99
+ # anonymise do
100
+ # selectors do
101
+ # for_subject(:user_id) { |user_id| self.select_for_user(user_id) }
102
+ # end
103
+ # end
104
+ #
105
+ # ModelName.anonymise_for!(:user_id, "user_1234")
106
+ def selectors(&block)
107
+ @selectors_config = Selectors.new(@model_class, &block)
108
+ end
109
+
110
+ def selector_for?(subject)
111
+ return false if @selectors_config.nil?
112
+
113
+ @selectors_config.selectors[subject].present?
114
+ end
115
+
89
116
  # Prevent any anonymisation strategy being applied when the provided block evaluates
90
117
  # to true. The block is executed in the model context.
91
118
  #
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anony
4
+ class NotAnonymisableException < StandardError
5
+ def initialize(record)
6
+ @record = record
7
+ super("Record does not implement anonymise!.
8
+ Have you included Anony::Anonymisable and a config?")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anony
4
+ class SelectorNotFoundException < StandardError
5
+ def initialize(selector, model_name)
6
+ super("Selector for #{selector} not found. Make sure you have one defined in #{model_name}")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "selector_not_found_exception"
4
+
5
+ module Anony
6
+ class Selectors
7
+ def initialize(model_class, &block)
8
+ @model_class = model_class
9
+ @selectors = {}
10
+ instance_exec(&block) if block
11
+ end
12
+
13
+ attr_reader :selectors
14
+
15
+ def for_subject(subject, &block)
16
+ selectors[subject] = block
17
+ end
18
+
19
+ def select(subject, subject_id)
20
+ selector = selectors[subject]
21
+ raise SelectorNotFoundException.new(subject.to_s, @model_class.name) if selector.nil?
22
+
23
+ @model_class.instance_exec(subject_id, &selector)
24
+ end
25
+ end
26
+ end
@@ -126,7 +126,7 @@ module Anony
126
126
 
127
127
  if already_ignored.any?
128
128
  raise ArgumentError, "Cannot ignore #{already_ignored.inspect} " \
129
- "(fields already ignored in Anony::Config)"
129
+ "(fields already ignored in Anony::Config)"
130
130
  end
131
131
 
132
132
  no_op(*fields)
data/lib/anony/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anony
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anony
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
  - GoCardless Engineering
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-16 00:00:00.000000000 Z
11
+ date: 2024-02-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 2.2.0
19
+ version: '2'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 2.2.0
26
+ version: '2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: gc_ruboconfig
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 2.24.0
33
+ version: 3.6.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 2.24.0
40
+ version: 3.6.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,19 +53,19 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.9'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec_junit_formatter
56
+ name: rspec-github
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.4'
61
+ version: 2.4.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.4'
68
+ version: 2.4.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: yard
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 1.4.1
89
+ version: 1.6.1
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 1.4.1
96
+ version: 1.6.1
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: activerecord
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -141,7 +141,8 @@ executables: []
141
141
  extensions: []
142
142
  extra_rdoc_files: []
143
143
  files:
144
- - ".circleci/config.yml"
144
+ - ".github/dependabot.yml"
145
+ - ".github/workflows/test.yml"
145
146
  - ".gitignore"
146
147
  - ".rubocop.yml"
147
148
  - ".rubocop_todo.yml"
@@ -161,8 +162,11 @@ files:
161
162
  - lib/anony/field_exception.rb
162
163
  - lib/anony/field_level_strategies.rb
163
164
  - lib/anony/model_config.rb
165
+ - lib/anony/not_anonymisable_exception.rb
164
166
  - lib/anony/result.rb
165
167
  - lib/anony/rspec_shared_examples.rb
168
+ - lib/anony/selector_not_found_exception.rb
169
+ - lib/anony/selectors.rb
166
170
  - lib/anony/skipped_exception.rb
167
171
  - lib/anony/strategies/destroy.rb
168
172
  - lib/anony/strategies/overwrite.rb
@@ -170,7 +174,8 @@ files:
170
174
  homepage: https://github.com/gocardless/anony
171
175
  licenses:
172
176
  - MIT
173
- metadata: {}
177
+ metadata:
178
+ rubygems_mfa_required: 'true'
174
179
  post_install_message:
175
180
  rdoc_options: []
176
181
  require_paths:
data/.circleci/config.yml DELETED
@@ -1,54 +0,0 @@
1
- version: 2.1
2
-
3
- jobs:
4
- test:
5
- parameters:
6
- ruby-version:
7
- type: string
8
- rails-version:
9
- type: string
10
-
11
- docker:
12
- - image: cimg/ruby:<<parameters.ruby-version>>
13
- environment:
14
- - RAILS_VERSION=<<parameters.rails-version>>
15
- steps:
16
- - checkout
17
-
18
- - restore_cache:
19
- key: anony-bundler-{{ checksum "anony.gemspec" }}
20
-
21
- - run: gem install bundler -v 2.2.33
22
- - run: bundle config set path 'vendor/bundle'
23
- - run: bundle install
24
-
25
- - save_cache:
26
- key: anony-bundler-{{ checksum "anony.gemspec" }}
27
- paths:
28
- - vendor/bundle
29
-
30
- - run:
31
- command: |
32
- bundle exec rspec --profile 10 \
33
- --format RspecJunitFormatter \
34
- --out /tmp/test-results/rspec.xml \
35
- --format progress \
36
- spec
37
-
38
- - store_test_results:
39
- path: /tmp/test-results
40
-
41
- - run: bundle exec rubocop --parallel --extra-details --display-style-guide
42
-
43
- workflows:
44
- version: 2
45
- tests:
46
- jobs:
47
- - test:
48
- matrix:
49
- parameters:
50
- ruby-version: ["2.6", "2.7", "3.0"]
51
- rails-version: ["6.0", "6.1", "7.0"]
52
- exclude:
53
- - ruby-version: "2.6"
54
- rails-version: "7.0"