anony 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: 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"