phi_attrs 0.2.1 → 0.2.4

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: f6271ebbfae4a5ddad7aa1ad27b812cc4056a5a4958b1067ddf2aa717975b738
4
- data.tar.gz: '098ec65389f50bbe24876657a1ff9eb78bbbe17fb5192804ebbc2f75eedd97f4'
3
+ metadata.gz: 1cc2ab95144c51b9ce2322864983ff9302d65d3e585c8ced98f1fe3098761173
4
+ data.tar.gz: 377a9e45f0069b4817a9156a66969b3056553707ae0f6a61716c16cbdc978a8a
5
5
  SHA512:
6
- metadata.gz: a5832351881d96019071f9b77ac4e08b991104dca6f47c8a1917f5c3546002d7510c5d0fe7ceaa4fe37df7006ec71105acbbcc99a5c9928e57e78b43a7cd2d5d
7
- data.tar.gz: 3a153bd1a992dbbb94de889f8a82c22f969dd1bb9b9f36254485d0265f70cba89d89f1c8b424e30056aa1c23c5b8e8be3142d7b5ccfed05c970ec67aa3614d69
6
+ metadata.gz: 78aa212eceac4e6b0ef10289ac0517b3d9943add98550fcffbc374e9d3d773ab3395d0cfae792161b6793f05fd7a08347e60b76201a6c719954956c902396fb2
7
+ data.tar.gz: 8fed8925c193caf3c169dff50800766d0abd9b45e6dd652de31b93b8219a2c3d425ac111a9c4b8608fbbfbc23a2e038f13bda2d89004a65fa801eb15dfcdbb37
@@ -11,15 +11,14 @@ jobs:
11
11
  ruby: [2.5, 2.6, 2.7]
12
12
 
13
13
  steps:
14
- - uses: actions/checkout@v2
14
+ - uses: actions/checkout@v3
15
15
  - name: Set up Ruby ${{ matrix.ruby }}
16
- uses: actions/setup-ruby@v1
16
+ uses: ruby/setup-ruby@v1
17
17
  with:
18
18
  ruby-version: ${{ matrix.ruby }}
19
+ bundler-cache: true
19
20
  - name: Install dependencies
20
21
  run: |
21
- gem install bundler
22
- bundle install
23
22
  bundle exec appraisal install
24
23
  - name: Run rspec
25
24
  run: bundler exec appraisal rspec
@@ -0,0 +1,25 @@
1
+ name: Publish Gem
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "main"
7
+ tags:
8
+ - v*
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: '2.6'
18
+ bundler-cache: true
19
+ - name: Release Gem
20
+ if: contains(github.ref, 'refs/tags/v')
21
+ uses: cadwallion/publish-rubygems-action@master
22
+ env:
23
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
24
+ RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
25
+ RELEASE_COMMAND: bundle exec rake release
data/README.md CHANGED
@@ -39,7 +39,7 @@ Or install it yourself as:
39
39
 
40
40
  ## Initialize
41
41
 
42
- Create an initializer to configure the PHI log file location.
42
+ Create an initializer to configure the PHI log file location. Log rotation can be configured with log_shift_age and log_shift_size (disabled by default).
43
43
 
44
44
  Example:
45
45
 
@@ -48,6 +48,8 @@ Example:
48
48
  ```ruby
49
49
  PhiAttrs.configure do |conf|
50
50
  conf.log_path = Rails.root.join("log", "phi_access_#{Rails.env}.log")
51
+ conf.log_shift_age = 10 # how many logs to keep of `log_shift_size` or frequency to rotate ('daily', 'weekly' or 'monthly'). Disable rotation with 0 (default).
52
+ conf.log_shift_size = 100.megabytes # size in bytes when using `log_shift_age` as a number
51
53
  end
52
54
  ```
53
55
 
@@ -275,7 +277,7 @@ There is also a block syntax of `disallow_phi` for temporary suppression phi acc
275
277
  ```ruby
276
278
  patient = PatientInfo.find(params[:id])
277
279
  patient.allow_phi!('allowed_user@example.com', 'Display Patient Data')
278
- patient.diallow_phi do
280
+ patient.disallow_phi do
279
281
  @data = patient.to_json # PHIAccessException
280
282
  end # Access is allowed again beyond this point
281
283
  ```
@@ -284,7 +286,7 @@ or a block level on a class:
284
286
 
285
287
  ```ruby
286
288
  PatientInfo.allow_phi!('allowed_user@example.com', 'Display Patient Data')
287
- PatientInfo.diallow_phi do
289
+ PatientInfo.disallow_phi do
288
290
  @data = PatientInfo.find(params[:id]).to_json # PHIAccessException
289
291
  end # Access is allowed again beyond this point
290
292
  ```
@@ -391,6 +393,27 @@ person_phi.allow_phi(nil, "Because I felt like looking at PHI") do
391
393
  end
392
394
  ```
393
395
 
396
+ ### Request UUID
397
+
398
+ It can be helpful to include the Rails request UUID to match up your general application
399
+ logs to your PHI access logs. The following snippet will prepend your PHI access logs
400
+ with the request UUID.
401
+
402
+ #### `app/controllers/application_controller.rb`
403
+
404
+ ```ruby
405
+ around_action :tag_phi_log_with_request_id
406
+
407
+ ...
408
+
409
+ private
410
+
411
+ def tag_phi_log_with_request_id
412
+ PhiAttrs::Logger.logger.tagged("Request ID: #{request.uuid}") do
413
+ yield
414
+ end
415
+ end
416
+ ```
394
417
  ## Best Practices
395
418
 
396
419
  * Mix and matching `instance`, `class` and `block` syntaxes for allowing/denying PHI is not recommended.
@@ -2,6 +2,8 @@
2
2
 
3
3
  module PhiAttrs
4
4
  @@log_path = nil
5
+ @@log_shift_age = 0 # Default to disabled
6
+ @@log_shift_size = 1048576 # 1MB - Default from logger class
5
7
  @@current_user_method = nil
6
8
  @@translation_prefix = 'phi'
7
9
 
@@ -17,6 +19,22 @@ module PhiAttrs
17
19
  @@log_path = value
18
20
  end
19
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
+
20
38
  def self.translation_prefix
21
39
  @@translation_prefix
22
40
  end
@@ -7,7 +7,7 @@ module PhiAttrs
7
7
  class << self
8
8
  def logger
9
9
  unless @logger
10
- logger = ActiveSupport::Logger.new(PhiAttrs.log_path)
10
+ logger = ActiveSupport::Logger.new(PhiAttrs.log_path, PhiAttrs.log_shift_age, PhiAttrs.log_shift_size)
11
11
  logger.formatter = Formatter.new
12
12
  @logger = ActiveSupport::TaggedLogging.new(logger)
13
13
  end
@@ -107,8 +107,32 @@ module PhiAttrs
107
107
  # end
108
108
  # # PHI Access Disallowed
109
109
  #
110
- def allow_phi(user_id = nil, reason = nil, allow_only: nil)
111
- raise ArgumentError, 'block required. use allow_phi! without block' unless block_given?
110
+ def allow_phi(user_id = nil, reason = nil, allow_only: nil, &block)
111
+ get_phi(user_id, reason, allow_only: allow_only, &block)
112
+ return
113
+ end
114
+
115
+
116
+ # Enable PHI access for any instance of this class in the block given only
117
+ # returning whatever the block returns.
118
+ #
119
+ # @param [String] user_id A unique identifier for the person accessing the PHI
120
+ # @param [String] reason The reason for accessing PHI
121
+ # @param [collection of PhiRecord] allow_only Specific PhiRecords to allow access to
122
+ # &block [block] The block in which PHI access is allowed for the class
123
+ #
124
+ # @example
125
+ # results = Foo.allow_phi('user@example.com', 'viewing patient record') do
126
+ # Foo.search(params)
127
+ # end
128
+ #
129
+ # @example
130
+ # loaded_foo = Foo.allow_phi('user@example.com', 'exporting patient list', allow_only: list_of_foos) do
131
+ # Bar.find_by(foo: list_of_foos).include(:foo)
132
+ # end
133
+ #
134
+ def get_phi(user_id = nil, reason = nil, allow_only: nil)
135
+ raise ArgumentError, 'block required' unless block_given?
112
136
 
113
137
  if allow_only.present?
114
138
  raise ArgumentError, 'allow_only must be iterable with each' unless allow_only.respond_to?(:each)
@@ -125,7 +149,7 @@ module PhiAttrs
125
149
  allow_only.each { |t| t.allow_phi!(user_id, reason) }
126
150
  end
127
151
 
128
- yield if block_given?
152
+ result = yield if block_given?
129
153
 
130
154
  __instances_with_extended_phi.each do |obj|
131
155
  if frozen_instances.include?(obj)
@@ -143,6 +167,8 @@ module PhiAttrs
143
167
  allow_only.each { |t| t.disallow_last_phi!(preserve_extensions: true) }
144
168
  # We've handled any newly extended allowances ourselves above
145
169
  end
170
+
171
+ result
146
172
  end
147
173
 
148
174
  # Explicitly disallow phi access in a specific area of code. This does not
@@ -335,17 +361,41 @@ module PhiAttrs
335
361
  # end
336
362
  # # PHI Access Disallowed Here
337
363
  #
338
- def allow_phi(user_id = nil, reason = nil)
339
- raise ArgumentError, 'block required. use allow_phi! without block' unless block_given?
364
+ def allow_phi(user_id = nil, reason = nil, &block)
365
+ get_phi(user_id, reason, &block)
366
+ return
367
+ end
368
+
369
+
370
+ # Enable PHI access for a single instance of this class inside the block.
371
+ # Returns whatever is returned from the block.
372
+ # Nested calls to get_phi will log once per nested call
373
+ #s
374
+ # @param [String] user_id A unique identifier for the person accessing the PHI
375
+ # @param [String] reason The reason for accessing PHI
376
+ # @yield The block in which phi access is allowed
377
+ #
378
+ # @return PHI
379
+ #
380
+ # @example
381
+ # foo = Foo.find(1)
382
+ # phi_data = foo.get_phi('user@example.com', 'viewing patient record') do
383
+ # foo.phi_field
384
+ # end
385
+ #
386
+ def get_phi(user_id = nil, reason = nil)
387
+ raise ArgumentError, 'block required' unless block_given?
340
388
 
341
389
  extended_instances = @__phi_relations_extended.clone
342
390
  allow_phi!(user_id, reason)
343
391
 
344
- yield if block_given?
392
+ result = yield if block_given?
345
393
 
346
394
  new_extensions = @__phi_relations_extended - extended_instances
347
395
  disallow_last_phi!(preserve_extensions: true)
348
396
  revoke_extended_phi!(new_extensions) if new_extensions.any?
397
+
398
+ result
349
399
  end
350
400
 
351
401
  # Revoke all PHI access for a single instance of this class.
@@ -412,6 +462,25 @@ module PhiAttrs
412
462
  end
413
463
  end
414
464
 
465
+
466
+ # The unique identifier for whom access has been allowed on this instance.
467
+ # This is what was passed in when PhiRecord#allow_phi! was called.
468
+ #
469
+ # @return [String] the user_id passed in to allow_phi!
470
+ #
471
+ def phi_allowed_by
472
+ phi_context[:user_id]
473
+ end
474
+
475
+ # The access reason for allowing access to this instance.
476
+ # This is what was passed in when PhiRecord#allow_phi! was called.
477
+ #
478
+ # @return [String] the reason passed in to allow_phi!
479
+ #
480
+ def phi_access_reason
481
+ phi_context[:reason]
482
+ end
483
+
415
484
  # Whether PHI access is allowed for a single instance of this class
416
485
  #
417
486
  # @example
@@ -424,6 +493,18 @@ module PhiAttrs
424
493
  !phi_context.nil? && phi_context[:phi_access_allowed]
425
494
  end
426
495
 
496
+ # Require phi access. Raises an error pre-emptively if it has not been granted.
497
+ #
498
+ # @example
499
+ # def use_phi(patient_record)
500
+ # patient_record.require_phi!
501
+ # # ...use PHI Freely
502
+ # end
503
+ #
504
+ def require_phi!
505
+ raise PhiAccessException, 'PHI Access required, please call allow_phi or allow_phi! first' unless phi_allowed?
506
+ end
507
+
427
508
  def reload
428
509
  @__phi_relations_extended.clear
429
510
  super
@@ -479,28 +560,6 @@ module PhiAttrs
479
560
  @__phi_log_keys = [PHI_ACCESS_LOG_TAG, self.class.name, @__phi_log_id]
480
561
  end
481
562
 
482
- # The unique identifier for whom access has been allowed on this instance.
483
- # This is what was passed in when PhiRecord#allow_phi! was called.
484
- #
485
- # @private
486
- #
487
- # @return [String] the user_id passed in to allow_phi!
488
- #
489
- def phi_allowed_by
490
- phi_context[:user_id]
491
- end
492
-
493
- # The access reason for allowing access to this instance.
494
- # This is what was passed in when PhiRecord#allow_phi! was called.
495
- #
496
- # @private
497
- #
498
- # @return [String] the reason passed in to allow_phi!
499
- #
500
- def phi_access_reason
501
- phi_context[:reason]
502
- end
503
-
504
563
  def phi_context
505
564
  instance_phi_context || class_phi_context
506
565
  end
@@ -0,0 +1,43 @@
1
+ require 'rspec/expectations'
2
+
3
+ DO_NOT_SPECIFY = "do not specify `allowed_by` or `with_access_reason` for negated `allow_phi_access`"
4
+
5
+ RSpec::Matchers.define :allow_phi_access do
6
+ match do |result|
7
+ @allowed = result.phi_allowed?
8
+ @user_id_matches = @user_id.nil? || @user_id == result.phi_allowed_by
9
+ @reason_matches = @reason.nil? || @reason == result.phi_access_reason
10
+
11
+ @allowed && @user_id_matches && @reason_matches
12
+ end
13
+
14
+ match_when_negated do |result|
15
+ raise ArgumentError, DO_NOT_SPECIFY unless @user_id.nil? && @reason.nil?
16
+
17
+ !result.phi_allowed?
18
+ end
19
+
20
+ chain :allowed_by do |user_id|
21
+ @user_id = user_id
22
+ end
23
+
24
+ chain :with_access_reason do |reason|
25
+ @reason = reason
26
+ end
27
+
28
+ # :nocov:
29
+ failure_message do |result|
30
+ msgs = []
31
+
32
+ msgs = ['PHI Access was not allowed.'] unless @allowed
33
+ msgs << "PHI Access was allowed by '#{result.phi_allowed_by}' (not '#{@user_id}')." unless @user_id_matches
34
+ msgs << "PHI Access was allowed because '#{result.phi_access_reason}' (not because '#{@reason}')." unless @reason_matches
35
+
36
+ msgs.join "\n"
37
+ end
38
+
39
+ failure_message_when_negated do |result|
40
+ "PHI access was allowed by '#{result.phi_allowed_by}', because '#{result.phi_access_reason}'"
41
+ end
42
+ # :nocov:
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhiAttrs
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.4'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phi_attrs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wyatt Kirby
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-10 00:00:00.000000000 Z
11
+ date: 2022-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -228,6 +228,7 @@ extensions: []
228
228
  extra_rdoc_files: []
229
229
  files:
230
230
  - ".github/workflows/build.yml"
231
+ - ".github/workflows/publish.yml"
231
232
  - ".gitignore"
232
233
  - ".rspec"
233
234
  - ".rubocop.yml"
@@ -259,6 +260,7 @@ files:
259
260
  - lib/phi_attrs/logger.rb
260
261
  - lib/phi_attrs/phi_record.rb
261
262
  - lib/phi_attrs/railtie.rb
263
+ - lib/phi_attrs/rspec.rb
262
264
  - lib/phi_attrs/version.rb
263
265
  - phi_attrs.gemspec
264
266
  homepage: http://www.apsis.io
@@ -283,7 +285,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
283
285
  - !ruby/object:Gem::Version
284
286
  version: '0'
285
287
  requirements: []
286
- rubygems_version: 3.0.3
288
+ rubygems_version: 3.3.26
287
289
  signing_key:
288
290
  specification_version: 4
289
291
  summary: PHI Access Restriction & Logging for Rails ActiveRecord