phi_attrs 0.3.2 → 0.4.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: bb209a550589eee5edbb17853c56c001c9ce8f7292784e1775cebb644468b830
4
- data.tar.gz: 0ca474072ecf6b33b2491ac77f097622e4611716d8c95fb03afee896615b8ac8
3
+ metadata.gz: 237044ea15d9e2cfbabb11218d60dbc32869ab91caa8c5f6f42f13eb2d9c34ff
4
+ data.tar.gz: 21b7e8149c94f9bc515767078bcf644976ebcda4e7c14209daec970b24ae8691
5
5
  SHA512:
6
- metadata.gz: 19021f005a8283d3db4da049c8278dd63b4e0cf5b229ddec2274b414501c9414347e8553d47bfdf5597d06f3da3d4ae2c679fe852d98c8a94e5299f7cb598f1e
7
- data.tar.gz: 8fdf599c5db471f842ee25fb91e50d5e54d3c9174978e709ebab5c6d45219a7bc8025f3b7060a51d425bc5fcd1176bcd7ea04df0054b50e8fb48b83c41658170
6
+ metadata.gz: 9cbaf5dcb370bd661ccbef997adbd68828289c9c7aa56e66c4773e765dffd2d65d41248991f3543b42fc09a73416fd9d8e38a9a3ce174fb1e95ddc89d268499c
7
+ data.tar.gz: e2c6245f0ef7843b4b55d3d0f2b4acf850ccfcc72755dc6482f09cb08e19efa8f0afdf2c5b26b56f1ee0bc5f7cec4128401764c54bb79905d767c93c05f9faaf
data/README.md CHANGED
@@ -204,12 +204,25 @@ patient.patient_info.first_name
204
204
 
205
205
  **NOTE:** This is not intended to be used on all relationships! Only those where you intend to grant implicit access based on access to another model. In this use case, we assume that allowed access to `Patient` implies allowed access to `PatientInfo`, and therefore does not require an additional `allow_phi!` check. There are no guaranteed safeguards against circular `extend_phi_access` calls!
206
206
 
207
+ ### New Records
208
+
209
+ PHI access is automatically allowed for new (unsaved) records. This allows attributes to be set during object creation without requiring an explicit `allow_phi!` call. Once a record has been persisted, normal PHI access controls apply.
210
+
211
+ ```ruby
212
+ patient = Patient.new
213
+ patient.phi_allowed? # => true
214
+ patient.first_name = 'Jane' # works without allow_phi!
215
+
216
+ patient.save!
217
+ patient.phi_allowed? # => false — must call allow_phi! after persisting
218
+ ```
219
+
207
220
  ### Check If PHI Access Is Allowed
208
221
 
209
222
  To check if PHI is allowed for a particular instance of a class call `phi_allowed?`.
210
223
 
211
224
  ```ruby
212
- patient = Patient.new
225
+ patient = Patient.find(1)
213
226
  patient.phi_allowed? # => false
214
227
 
215
228
  patient.allow_phi('user@example.com', 'reason') do
@@ -430,16 +443,20 @@ It is recommended to use the provided `docker-compose` environment for developme
430
443
 
431
444
  ### Tests
432
445
 
433
- Tests are written using [RSpec](http://rspec.info/) and are setup to use [Appraisal](https://github.com/thoughtbot/appraisal) to run tests over multiple rails versions.
446
+ Tests are written using [RSpec](https://rspec.info/) and are setup to use [Appraisal](https://github.com/thoughtbot/appraisal) to run tests over multiple Rails versions. Supported runtimes are Ruby 3.2+ and Rails 7.0 through 8.0.
434
447
 
435
448
  $ bin/run_tests
436
449
  or for individual tests:
437
450
  $ bin/ssh_to_container
438
451
  $ bundle exec appraisal rspec spec/path/to/spec.rb
439
452
 
440
- To run just a particular rails version:
441
- $ bundle exec appraisal rails_6.1 rspec
442
- $ bundle exec appraisal rails-7.0 rspec
453
+ To run just a particular Rails version:
454
+
455
+ $ bundle exec appraisal rails_7.0 rspec
456
+
457
+ Linting uses [standardrb](https://github.com/standardrb/standard):
458
+
459
+ $ bundle exec standardrb
443
460
 
444
461
  ### Console
445
462
 
@@ -450,13 +467,38 @@ An interactive prompt that will allow you to experiment with the gem.
450
467
 
451
468
  ### Local Install
452
469
 
453
- Run `bin/setup` to install dependencies. Then, run `bundle exec appraisal rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
470
+ Run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run linting and tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
454
471
 
455
472
  To install this gem onto your local machine, run `bundle exec rake install`.
456
473
 
457
- ### Versioning
474
+ ### Releases
475
+
476
+ Releases are driven by git tags. The version lives in `lib/phi_attrs/version.rb`, and the gemspec reads `PhiAttrs::VERSION`.
458
477
 
459
- To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
478
+ ```bash
479
+ bundle install
480
+ bin/release patch # or: minor, major
481
+ ```
482
+
483
+ To publish an RC for a specific version:
484
+
485
+ ```bash
486
+ bin/release rc 1.2.0
487
+ ```
488
+
489
+ This sets the version to `1.2.0-rc` and tags it as `v1.2.0-rc`.
490
+
491
+ Prereleases can also use `bump`'s native `pre` increment:
492
+
493
+ ```bash
494
+ bin/release pre
495
+ ```
496
+
497
+ `pre` cycles prerelease labels in order: `alpha`, `beta`, `rc`, then the final version. For example, `1.2.0-rc` becomes `1.2.0`, tagged as `v1.2.0`.
498
+
499
+ `bin/release` uses `bump`, commits the version file, creates a `v<version>` tag, pushes the branch, and pushes the tag.
500
+
501
+ GitHub Actions publishes only when a `v*` tag is pushed. The publish workflow builds the gem, pushes it to RubyGems with `RUBYGEMS_API_KEY`, and creates a GitHub release with the built gem attached. Tags containing a prerelease suffix, such as `v1.2.0-rc`, are marked as prereleases on GitHub.
460
502
 
461
503
 
462
504
  ## Contributing
@@ -465,6 +507,12 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/apsisl
465
507
 
466
508
  Any PRs should be accompanied with documentation in `README.md`.
467
509
 
510
+ ### Releasing
511
+
512
+ * Squash and merge your PR.
513
+ * Run `bin/release patch`, `bin/release minor`, `bin/release major`, or `bin/release rc X.Y.Z`.
514
+ * The pushed `v*` tag will be automatically built and published to RubyGems.
515
+
468
516
  ## License
469
517
 
470
518
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,53 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhiAttrs
4
- @@log_path = nil
5
- @@log_shift_age = 0 # Default to disabled
6
- @@log_shift_size = 1_048_576 # 1MB - Default from logger class
7
- @@current_user_method = nil
8
- @@translation_prefix = 'phi'
4
+ mattr_accessor :log_path, default: nil
5
+ mattr_accessor :log_shift_age, default: 0
6
+ mattr_accessor :log_shift_size, default: 1_048_576
7
+ mattr_accessor :current_user_method, default: nil
8
+ mattr_accessor :translation_prefix, default: "phi"
9
9
 
10
10
  def self.configure
11
11
  yield self if block_given?
12
12
  end
13
-
14
- def self.log_path
15
- @@log_path
16
- end
17
-
18
- def self.log_path=(value)
19
- @@log_path = value
20
- end
21
-
22
- def self.log_shift_age
23
- @@log_shift_age
24
- end
25
-
26
- def self.log_shift_age=(value)
27
- @@log_shift_age = value
28
- end
29
-
30
- def self.log_shift_size
31
- @@log_shift_size
32
- end
33
-
34
- def self.log_shift_size=(value)
35
- @@log_shift_size = value
36
- end
37
-
38
- def self.translation_prefix
39
- @@translation_prefix
40
- end
41
-
42
- def self.translation_prefix=(value)
43
- @@translation_prefix = value
44
- end
45
-
46
- def self.current_user_method
47
- @@current_user_method
48
- end
49
-
50
- def self.current_user_method=(value)
51
- @@current_user_method = value
52
- end
53
13
  end
@@ -3,11 +3,11 @@
3
3
  module PhiAttrs
4
4
  module Exceptions
5
5
  class PhiAccessException < StandardError
6
- TAG = 'UNAUTHORIZED ACCESS'
6
+ TAG = "UNAUTHORIZED ACCESS"
7
7
 
8
8
  def initialize(msg)
9
9
  PhiAttrs::Logger.tagged(TAG) { PhiAttrs::Logger.error(msg) }
10
- super(msg)
10
+ super
11
11
  end
12
12
  end
13
13
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhiAttrs
4
- FORMAT = "%s %5s: %s\n"
5
-
6
4
  # https://github.com/ruby/ruby/blob/trunk/lib/logger.rb#L587
7
5
  class Formatter < ::Logger::Formatter
6
+ FORMAT = "%s %5s: %s\n"
7
+
8
8
  def call(severity, timestamp, _progname, msg)
9
9
  format(FORMAT, format_datetime(timestamp), severity, msg2str(msg))
10
10
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhiAttrs
4
- PHI_ACCESS_LOG_TAG = 'PHI Access Log'
4
+ PHI_ACCESS_LOG_TAG = "PHI Access Log"
5
5
 
6
6
  class Logger
7
7
  class << self
@@ -2,6 +2,8 @@
2
2
 
3
3
  # Namespace for classes and modules that handle PHI Attribute Access Logging
4
4
  module PhiAttrs
5
+ PhiStackEntry = Struct.new(:phi_access_allowed, :user_id, :reason, :logged, :uuid, keyword_init: true)
6
+
5
7
  # Module for extending ActiveRecord models to handle PHI access logging
6
8
  # and restrict access to attributes.
7
9
  #
@@ -77,13 +79,17 @@ module PhiAttrs
77
79
  reason ||= i18n_reason
78
80
  raise ArgumentError, 'user_id and reason cannot be blank' if user_id.blank? || reason.blank?
79
81
 
80
- __phi_stack.push({
81
- phi_access_allowed: true,
82
- user_id: user_id,
83
- reason: reason
84
- })
85
-
86
- PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do
82
+ user_id = user_id.to_s.gsub(/[\r\n]/, " ")
83
+ reason = reason.to_s.gsub(/[\r\n]/, " ")
84
+ uuid = SecureRandom.uuid
85
+ __phi_stack.push(PhiStackEntry.new(
86
+ phi_access_allowed: true,
87
+ user_id: user_id,
88
+ reason: reason,
89
+ uuid: uuid
90
+ ))
91
+
92
+ PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name, uuid) do
87
93
  PhiAttrs::Logger.info("PHI Access Enabled for '#{user_id}': #{reason}")
88
94
  end
89
95
  end
@@ -109,7 +115,7 @@ module PhiAttrs
109
115
  #
110
116
  def allow_phi(user_id = nil, reason = nil, allow_only: nil, &block)
111
117
  get_phi(user_id, reason, allow_only: allow_only, &block)
112
- return
118
+ nil
113
119
  end
114
120
 
115
121
  # Enable PHI access for any instance of this class in the block given only
@@ -149,7 +155,7 @@ module PhiAttrs
149
155
  allow_only.each { |t| t.allow_phi!(user_id, reason) }
150
156
  end
151
157
 
152
- return yield
158
+ yield
153
159
  ensure
154
160
  __instances_with_extended_phi.each do |obj|
155
161
  if frozen_instances.include?(obj)
@@ -193,13 +199,12 @@ module PhiAttrs
193
199
  def disallow_phi
194
200
  raise ArgumentError, 'block required. use disallow_phi! without block' unless block_given?
195
201
 
196
- __phi_stack.push({
197
- phi_access_allowed: false
198
- })
199
-
200
- yield if block_given?
201
-
202
- __phi_stack.pop
202
+ __phi_stack.push(PhiStackEntry.new(phi_access_allowed: false))
203
+ begin
204
+ yield
205
+ ensure
206
+ __phi_stack.pop
207
+ end
203
208
  end
204
209
 
205
210
  # Revoke all PHI access for this class, if enabled by PhiRecord#allow_phi!
@@ -210,11 +215,12 @@ module PhiAttrs
210
215
  def disallow_phi!
211
216
  raise ArgumentError, 'block not allowed. use disallow_phi with block' if block_given?
212
217
 
218
+ uuid = __uuid_string(__phi_stack)
213
219
  message = __phi_stack.present? ? "PHI access disabled for #{__user_id_string(__phi_stack)}" : 'PHI access disabled. No class level access was granted.'
214
220
 
215
221
  __reset_phi_stack
216
222
 
217
- PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do
223
+ PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name, uuid) do
218
224
  PhiAttrs::Logger.info(message)
219
225
  end
220
226
  end
@@ -228,9 +234,10 @@ module PhiAttrs
228
234
  raise ArgumentError, 'block not allowed' if block_given?
229
235
 
230
236
  removed_access = __phi_stack.pop
231
- message = removed_access.present? ? "PHI access disabled for #{removed_access[:user_id]}" : 'PHI access disabled. No class level access was granted.'
237
+ uuid = __uuid_string(removed_access)
238
+ message = removed_access.present? ? "PHI access disabled for #{removed_access.user_id}" : 'PHI access disabled. No class level access was granted.'
232
239
 
233
- PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do
240
+ PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name, uuid) do
234
241
  PhiAttrs::Logger.info(message)
235
242
  end
236
243
  end
@@ -243,7 +250,7 @@ module PhiAttrs
243
250
  # @return [Boolean] whether PHI access is allowed for this instance
244
251
  #
245
252
  def phi_allowed?
246
- __phi_stack.present? && __phi_stack[-1][:phi_access_allowed]
253
+ __phi_stack.present? && __phi_stack[-1].phi_access_allowed
247
254
  end
248
255
 
249
256
  def __instances_with_extended_phi
@@ -262,7 +269,12 @@ module PhiAttrs
262
269
 
263
270
  def __user_id_string(access_list)
264
271
  access_list ||= []
265
- access_list.map { |c| "'#{c[:user_id]}'" }.join(',')
272
+ access_list.map { |c| "'#{c.user_id}'" }.join(',')
273
+ end
274
+
275
+ def __uuid_string(access_list)
276
+ access_list ||= []
277
+ Array.wrap(access_list).map(&:uuid).join(',').presence || 'none'
266
278
  end
267
279
 
268
280
  def current_user
@@ -335,13 +347,17 @@ module PhiAttrs
335
347
  reason ||= self.class.i18n_reason
336
348
  raise ArgumentError, 'user_id and reason cannot be blank' if user_id.blank? || reason.blank?
337
349
 
338
- PhiAttrs::Logger.tagged(*phi_log_keys) do
339
- @__phi_access_stack.push({
340
- phi_access_allowed: true,
341
- user_id: user_id,
342
- reason: reason
343
- })
344
-
350
+ user_id = user_id.to_s.gsub(/[\r\n]/, " ")
351
+ reason = reason.to_s.gsub(/[\r\n]/, " ")
352
+ uuid = SecureRandom.uuid
353
+ @__phi_access_stack.push(PhiStackEntry.new(
354
+ phi_access_allowed: true,
355
+ user_id: user_id,
356
+ reason: reason,
357
+ uuid: uuid,
358
+ ))
359
+
360
+ PhiAttrs::Logger.tagged(*phi_log_keys, uuid) do
345
361
  PhiAttrs::Logger.info("PHI Access Enabled for '#{user_id}': #{reason}")
346
362
  end
347
363
  end
@@ -362,7 +378,7 @@ module PhiAttrs
362
378
  #
363
379
  def allow_phi(user_id = nil, reason = nil, &block)
364
380
  get_phi(user_id, reason, &block)
365
- return
381
+ nil
366
382
  end
367
383
 
368
384
  # Enable PHI access for a single instance of this class inside the block.
@@ -388,7 +404,7 @@ module PhiAttrs
388
404
  begin
389
405
  allow_phi!(user_id, reason)
390
406
 
391
- return yield
407
+ yield
392
408
  ensure
393
409
  new_extensions = @__phi_relations_extended - extended_instances
394
410
  disallow_last_phi!(preserve_extensions: true)
@@ -405,7 +421,9 @@ module PhiAttrs
405
421
  def disallow_phi!
406
422
  raise ArgumentError, 'block not allowed. use disallow_phi with block' if block_given?
407
423
 
408
- PhiAttrs::Logger.tagged(*phi_log_keys) do
424
+ removed_access_for_uuid = self.class.__uuid_string(@__phi_access_stack)
425
+
426
+ PhiAttrs::Logger.tagged(*phi_log_keys, removed_access_for_uuid) do
409
427
  removed_access_for = self.class.__user_id_string(@__phi_access_stack)
410
428
 
411
429
  revoke_extended_phi!
@@ -435,11 +453,12 @@ module PhiAttrs
435
453
 
436
454
  add_disallow_flag!
437
455
  add_disallow_flag_to_extended_phi!
438
-
439
- yield if block_given?
440
-
441
- remove_disallow_flag_from_extended_phi!
442
- remove_disallow_flag!
456
+ begin
457
+ yield
458
+ ensure
459
+ remove_disallow_flag_from_extended_phi!
460
+ remove_disallow_flag!
461
+ end
443
462
  end
444
463
 
445
464
  # Revoke last PHI access for a single instance of this class.
@@ -451,11 +470,12 @@ module PhiAttrs
451
470
  def disallow_last_phi!(preserve_extensions: false)
452
471
  raise ArgumentError, 'block not allowed' if block_given?
453
472
 
454
- PhiAttrs::Logger.tagged(*phi_log_keys) do
455
- removed_access = @__phi_access_stack.pop
473
+ removed_access = @__phi_access_stack.pop
474
+ uuid = removed_access&.uuid
456
475
 
476
+ PhiAttrs::Logger.tagged(*phi_log_keys, uuid) do
457
477
  revoke_extended_phi! unless preserve_extensions
458
- message = removed_access.present? ? "PHI access disabled for #{removed_access[:user_id]}" : 'PHI access disabled. No instance level access was granted.'
478
+ message = removed_access.present? ? "PHI access disabled for #{removed_access.user_id}" : 'PHI access disabled. No instance level access was granted.'
459
479
  PhiAttrs::Logger.info(message)
460
480
  end
461
481
  end
@@ -466,7 +486,9 @@ module PhiAttrs
466
486
  # @return [String] the user_id passed in to allow_phi!
467
487
  #
468
488
  def phi_allowed_by
469
- phi_context[:user_id]
489
+ return '__new_record__' if new_record?
490
+
491
+ phi_context&.user_id
470
492
  end
471
493
 
472
494
  # The access reason for allowing access to this instance.
@@ -475,7 +497,9 @@ module PhiAttrs
475
497
  # @return [String] the reason passed in to allow_phi!
476
498
  #
477
499
  def phi_access_reason
478
- phi_context[:reason]
500
+ return 'new record' if new_record?
501
+
502
+ phi_context&.reason
479
503
  end
480
504
 
481
505
  # Whether PHI access is allowed for a single instance of this class
@@ -487,7 +511,7 @@ module PhiAttrs
487
511
  # @return [Boolean] whether PHI access is allowed for this instance
488
512
  #
489
513
  def phi_allowed?
490
- !phi_context.nil? && phi_context[:phi_access_allowed]
514
+ new_record? || (!phi_context.nil? && phi_context.phi_access_allowed)
491
515
  end
492
516
 
493
517
  # Require phi access. Raises an error pre-emptively if it has not been granted.
@@ -499,7 +523,7 @@ module PhiAttrs
499
523
  # end
500
524
  #
501
525
  def require_phi!
502
- raise PhiAccessException, 'PHI Access required, please call allow_phi or allow_phi! first' unless phi_allowed?
526
+ raise PhiAttrs::Exceptions::PhiAccessException, 'PHI Access required, please call allow_phi or allow_phi! first' unless phi_allowed?
503
527
  end
504
528
 
505
529
  def reload
@@ -512,9 +536,7 @@ module PhiAttrs
512
536
  # Adds a disallow phi flag to instance internal stack.
513
537
  # @private since subject to change
514
538
  def add_disallow_flag!
515
- @__phi_access_stack.push({
516
- phi_access_allowed: false
517
- })
539
+ @__phi_access_stack.push(PhiStackEntry.new(phi_access_allowed: false))
518
540
  end
519
541
 
520
542
  # removes the last item in instance internal stack.
@@ -553,8 +575,13 @@ module PhiAttrs
553
575
  # @return [Array<String>] log key for an instance of this class
554
576
  #
555
577
  def phi_log_keys
556
- @__phi_log_id = persisted? ? "Key: #{attributes[self.class.primary_key]}" : "Object: #{object_id}"
557
- @__phi_log_keys = [PHI_ACCESS_LOG_TAG, self.class.name, @__phi_log_id]
578
+ current_id = persisted? ? public_send(self.class.primary_key) : object_id
579
+ if @__phi_log_cached_id != current_id
580
+ @__phi_log_cached_id = current_id
581
+ prefix = persisted? ? "Key: #{current_id}" : "Object: #{current_id}"
582
+ @__phi_log_keys = [PHI_ACCESS_LOG_TAG, self.class.name, prefix].freeze
583
+ end
584
+ @__phi_log_keys
558
585
  end
559
586
 
560
587
  def phi_context
@@ -576,19 +603,19 @@ module PhiAttrs
576
603
  # @return String of all the user_id's passed in to allow_phi!
577
604
  #
578
605
  def all_phi_allowed_by
579
- self.class.__user_id_string(all_phi_context)
580
- end
581
-
582
- def all_phi_context
583
- (@__phi_access_stack || []) + (self.class.__phi_stack || [])
606
+ ids = (@__phi_access_stack || []).filter_map { |c| "'#{c.user_id}'" }
607
+ ids.concat(self.class.__phi_stack.filter_map { |c| "'#{c.user_id}'" })
608
+ ids.join(',')
584
609
  end
585
610
 
586
611
  def all_phi_context_logged?
587
- all_phi_context.all? { |v| v[:logged] }
612
+ (@__phi_access_stack.nil? || @__phi_access_stack.all? { |v| v.logged }) &&
613
+ self.class.__phi_stack.all? { |v| v.logged }
588
614
  end
589
615
 
590
616
  def set_all_phi_context_logged
591
- all_phi_context.each { |c| c[:logged] = true }
617
+ @__phi_access_stack&.each { |c| c.logged = true }
618
+ self.class.__phi_stack.each { |c| c.logged = true }
592
619
  end
593
620
 
594
621
  # Core logic for wrapping methods in PHI access logging and access restriction.
@@ -625,7 +652,7 @@ module PhiAttrs
625
652
  #
626
653
  def phi_wrap_method(method_name)
627
654
  unless respond_to?(method_name)
628
- PhiAttrs::Logger.warn("#{self.class.name} tried to wrap non-existant method (#{method_name})")
655
+ PhiAttrs::Logger.warn("#{self.class.name} tried to wrap non-existent method (#{method_name})")
629
656
  return
630
657
  end
631
658
  return if self.class.__phi_methods_wrapped.include? method_name
@@ -633,23 +660,26 @@ module PhiAttrs
633
660
  wrapped_method = :"__#{method_name}_phi_wrapped"
634
661
  unwrapped_method = :"__#{method_name}_phi_unwrapped"
635
662
 
636
- self.class.send(:define_method, wrapped_method) do |*args, **kwargs, &block|
637
- PhiAttrs::Logger.tagged(*phi_log_keys) do
638
- unless phi_allowed?
639
- raise PhiAttrs::Exceptions::PhiAccessException, "Attempted PHI access for #{self.class.name} #{@__phi_user_id}"
640
- end
663
+ unless self.class.method_defined?(wrapped_method)
664
+ self.class.send(:define_method, wrapped_method) do |*args, **kwargs, &block|
665
+ uuid = self.class.__uuid_string(@__phi_access_stack)
666
+ PhiAttrs::Logger.tagged(*phi_log_keys, uuid) do
667
+ unless phi_allowed?
668
+ raise PhiAttrs::Exceptions::PhiAccessException, "Attempted PHI access for #{self.class.name} #{@__phi_user_id}"
669
+ end
641
670
 
642
- unless all_phi_context_logged?
643
- PhiAttrs::Logger.info("#{self.class.name} access by [#{all_phi_allowed_by}]. Triggered by method: #{method_name}")
644
- set_all_phi_context_logged
645
- end
671
+ unless all_phi_context_logged?
672
+ PhiAttrs::Logger.info("#{self.class.name} access by [#{all_phi_allowed_by}]. Triggered by method: #{method_name}")
673
+ set_all_phi_context_logged
674
+ end
646
675
 
647
- send(unwrapped_method, *args, **kwargs, &block)
676
+ send(unwrapped_method, *args, **kwargs, &block)
677
+ end
648
678
  end
649
679
  end
650
680
 
651
681
  # method_name => wrapped_method => unwrapped_method
652
- self.class.send(:alias_method, unwrapped_method, method_name)
682
+ self.class.send(:alias_method, unwrapped_method, method_name) unless self.class.method_defined?(unwrapped_method)
653
683
  self.class.send(:alias_method, method_name, wrapped_method)
654
684
 
655
685
  self.class.__phi_methods_wrapped << method_name
@@ -669,23 +699,25 @@ module PhiAttrs
669
699
  wrapped_method = wrapped_extended_name(method_name)
670
700
  unwrapped_method = unwrapped_extended_name(method_name)
671
701
 
672
- self.class.send(:define_method, wrapped_method) do |*args, **kwargs, &block|
673
- relation = send(unwrapped_method, *args, **kwargs, &block)
702
+ unless self.class.method_defined?(wrapped_method)
703
+ self.class.send(:define_method, wrapped_method) do |*args, **kwargs, &block|
704
+ relation = send(unwrapped_method, *args, **kwargs, &block)
674
705
 
675
- if phi_allowed? && (relation.present? && relation_klass(relation).included_modules.include?(PhiRecord))
676
- relations = relation.is_a?(Enumerable) ? relation : [relation]
677
- relations.each do |r|
678
- r.allow_phi!(phi_allowed_by, phi_access_reason) unless @__phi_relations_extended.include?(r)
706
+ if phi_allowed? && relation.present? && relation_klass(relation).included_modules.include?(PhiRecord)
707
+ relations = relation.is_a?(Enumerable) ? relation : [relation]
708
+ relations.each do |r|
709
+ r.allow_phi!(phi_allowed_by, phi_access_reason) unless @__phi_relations_extended.include?(r)
710
+ end
711
+ @__phi_relations_extended.merge(relations)
712
+ self.class.__instances_with_extended_phi.add(self)
679
713
  end
680
- @__phi_relations_extended.merge(relations)
681
- self.class.__instances_with_extended_phi.add(self)
682
- end
683
714
 
684
- relation
715
+ relation
716
+ end
685
717
  end
686
718
 
687
719
  # method_name => wrapped_method => unwrapped_method
688
- self.class.send(:alias_method, unwrapped_method, method_name)
720
+ self.class.send(:alias_method, unwrapped_method, method_name) unless self.class.method_defined?(unwrapped_method)
689
721
  self.class.send(:alias_method, method_name, wrapped_method)
690
722
 
691
723
  self.class.__phi_methods_to_extend << method_name
@@ -720,7 +752,7 @@ module PhiAttrs
720
752
  return rel.klass if rel.is_a?(ActiveRecord::Relation)
721
753
  return rel.first.class if rel.is_a?(Enumerable)
722
754
 
723
- return rel.class
755
+ rel.class
724
756
  end
725
757
 
726
758
  def wrapped_extended_name(method_name)
@@ -1,15 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'phi_attrs'
4
- require 'rails'
3
+ require "phi_attrs"
4
+ require "rails"
5
5
 
6
6
  module PhiAttrs
7
7
  class Railtie < Rails::Railtie
8
- initializer 'phi_attrs.initialize' do |_app|
9
- ActiveSupport.on_load(:active_record) do
10
- ActiveSupport.on_load(:active_record) { extend PhiAttrs::Model }
11
- ActiveSupport.on_load(:action_controller) { include PhiAttrs::Controller }
12
- end
8
+ initializer "phi_attrs.initialize" do |_app|
9
+ ActiveSupport.on_load(:active_record) { extend PhiAttrs::Model }
10
+ ActiveSupport.on_load(:action_controller) { include PhiAttrs::Controller }
13
11
  end
14
12
  end
15
13
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rspec/expectations'
3
+ require "rspec/expectations"
4
4
 
5
- DO_NOT_SPECIFY = 'do not specify `allowed_by` or `with_access_reason` for negated `allow_phi_access`'
5
+ DO_NOT_SPECIFY = "do not specify `allowed_by` or `with_access_reason` for negated `allow_phi_access`"
6
6
 
7
7
  RSpec::Matchers.define :allow_phi_access do
8
8
  match do |result|
@@ -31,7 +31,7 @@ RSpec::Matchers.define :allow_phi_access do
31
31
  failure_message do |result|
32
32
  msgs = []
33
33
 
34
- msgs = ['PHI Access was not allowed.'] unless @allowed
34
+ msgs = ["PHI Access was not allowed."] unless @allowed
35
35
  msgs << "PHI Access was allowed by '#{result.phi_allowed_by}' (not '#{@user_id}')." unless @user_id_matches
36
36
  msgs << "PHI Access was allowed because '#{result.phi_access_reason}' (not because '#{@reason}')." unless @reason_matches
37
37
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhiAttrs
4
- VERSION = '0.3.2'
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/phi_attrs.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'rails'
4
4
  require 'active_support'
5
5
  require 'request_store'
6
+ require 'securerandom'
6
7
 
7
8
  require 'phi_attrs/version'
8
9
  require 'phi_attrs/configure'
metadata CHANGED
@@ -1,29 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phi_attrs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wyatt Kirby
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-10-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: benchmark
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: rails
15
28
  requirement: !ruby/object:Gem::Requirement
16
29
  requirements:
17
30
  - - ">="
18
31
  - !ruby/object:Gem::Version
19
- version: 6.0.0
32
+ version: 7.0.0
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '9.0'
20
36
  type: :runtime
21
37
  prerelease: false
22
38
  version_requirements: !ruby/object:Gem::Requirement
23
39
  requirements:
24
40
  - - ">="
25
41
  - !ruby/object:Gem::Version
26
- version: 6.0.0
42
+ version: 7.0.0
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '9.0'
27
46
  - !ruby/object:Gem::Dependency
28
47
  name: request_store
29
48
  requirement: !ruby/object:Gem::Requirement
@@ -40,6 +59,20 @@ dependencies:
40
59
  version: '1.4'
41
60
  - !ruby/object:Gem::Dependency
42
61
  name: appraisal
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.5'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '2.5'
74
+ - !ruby/object:Gem::Dependency
75
+ name: bump
43
76
  requirement: !ruby/object:Gem::Requirement
44
77
  requirements:
45
78
  - - ">="
@@ -58,16 +91,16 @@ dependencies:
58
91
  requirements:
59
92
  - - ">="
60
93
  - !ruby/object:Gem::Version
61
- version: 2.2.33
94
+ version: '2.4'
62
95
  type: :development
63
96
  prerelease: false
64
97
  version_requirements: !ruby/object:Gem::Requirement
65
98
  requirements:
66
99
  - - ">="
67
100
  - !ruby/object:Gem::Version
68
- version: 2.2.33
101
+ version: '2.4'
69
102
  - !ruby/object:Gem::Dependency
70
- name: byebug
103
+ name: debug
71
104
  requirement: !ruby/object:Gem::Requirement
72
105
  requirements:
73
106
  - - ">="
@@ -110,6 +143,34 @@ dependencies:
110
143
  version: '0'
111
144
  - !ruby/object:Gem::Dependency
112
145
  name: rake
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '13.3'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '13.3'
158
+ - !ruby/object:Gem::Dependency
159
+ name: rspec
160
+ requirement: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - "~>"
163
+ - !ruby/object:Gem::Version
164
+ version: '3.13'
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - "~>"
170
+ - !ruby/object:Gem::Version
171
+ version: '3.13'
172
+ - !ruby/object:Gem::Dependency
173
+ name: rspec-rails
113
174
  requirement: !ruby/object:Gem::Requirement
114
175
  requirements:
115
176
  - - ">="
@@ -123,7 +184,7 @@ dependencies:
123
184
  - !ruby/object:Gem::Version
124
185
  version: '0'
125
186
  - !ruby/object:Gem::Dependency
126
- name: rubocop
187
+ name: simplecov
127
188
  requirement: !ruby/object:Gem::Requirement
128
189
  requirements:
129
190
  - - ">="
@@ -137,7 +198,7 @@ dependencies:
137
198
  - !ruby/object:Gem::Version
138
199
  version: '0'
139
200
  - !ruby/object:Gem::Dependency
140
- name: rubocop-rails
201
+ name: sqlite3
141
202
  requirement: !ruby/object:Gem::Requirement
142
203
  requirements:
143
204
  - - ">="
@@ -151,19 +212,19 @@ dependencies:
151
212
  - !ruby/object:Gem::Version
152
213
  version: '0'
153
214
  - !ruby/object:Gem::Dependency
154
- name: simplecov
215
+ name: standardrb
155
216
  requirement: !ruby/object:Gem::Requirement
156
217
  requirements:
157
- - - "~>"
218
+ - - ">="
158
219
  - !ruby/object:Gem::Version
159
- version: '0.16'
220
+ version: '0'
160
221
  type: :development
161
222
  prerelease: false
162
223
  version_requirements: !ruby/object:Gem::Requirement
163
224
  requirements:
164
- - - "~>"
225
+ - - ">="
165
226
  - !ruby/object:Gem::Version
166
- version: '0.16'
227
+ version: '0'
167
228
  - !ruby/object:Gem::Dependency
168
229
  name: tzinfo-data
169
230
  requirement: !ruby/object:Gem::Requirement
@@ -178,7 +239,6 @@ dependencies:
178
239
  - - ">="
179
240
  - !ruby/object:Gem::Version
180
241
  version: '0'
181
- description:
182
242
  email:
183
243
  - wyatt@apsis.io
184
244
  executables: []
@@ -197,14 +257,14 @@ files:
197
257
  - lib/phi_attrs/railtie.rb
198
258
  - lib/phi_attrs/rspec.rb
199
259
  - lib/phi_attrs/version.rb
200
- homepage: http://www.apsis.io
260
+ homepage: https://www.apsis.io
201
261
  licenses:
202
262
  - MIT
203
263
  metadata:
204
264
  rubygems_mfa_required: 'false'
205
265
  post_install_message: "\n Thank you for installing phi_attrs! By installing this
206
266
  gem,\n you acknowledge and agree to the disclaimer as provided in the\n DISCLAIMER.txt
207
- file.\n\n For full details, see: https://github.com/apsislabs/phi_attrs/blob/master/DISCLAIMER.txt\n
267
+ file.\n\n For full details, see: https://github.com/apsislabs/phi_attrs/blob/main/DISCLAIMER.txt\n
208
268
  \ "
209
269
  rdoc_options: []
210
270
  require_paths:
@@ -213,15 +273,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
213
273
  requirements:
214
274
  - - ">="
215
275
  - !ruby/object:Gem::Version
216
- version: 2.7.0
276
+ version: 3.2.0
217
277
  required_rubygems_version: !ruby/object:Gem::Requirement
218
278
  requirements:
219
279
  - - ">="
220
280
  - !ruby/object:Gem::Version
221
281
  version: '0'
222
282
  requirements: []
223
- rubygems_version: 3.3.27
224
- signing_key:
283
+ rubygems_version: 3.6.9
225
284
  specification_version: 4
226
285
  summary: PHI Access Restriction & Logging for Rails ActiveRecord
227
286
  test_files: []