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 +4 -4
- data/.github/workflows/build.yml +3 -4
- data/.github/workflows/publish.yml +25 -0
- data/README.md +26 -3
- data/lib/phi_attrs/configure.rb +18 -0
- data/lib/phi_attrs/logger.rb +1 -1
- data/lib/phi_attrs/phi_record.rb +87 -28
- data/lib/phi_attrs/rspec.rb +43 -0
- data/lib/phi_attrs/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1cc2ab95144c51b9ce2322864983ff9302d65d3e585c8ced98f1fe3098761173
|
4
|
+
data.tar.gz: 377a9e45f0069b4817a9156a66969b3056553707ae0f6a61716c16cbdc978a8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 78aa212eceac4e6b0ef10289ac0517b3d9943add98550fcffbc374e9d3d773ab3395d0cfae792161b6793f05fd7a08347e60b76201a6c719954956c902396fb2
|
7
|
+
data.tar.gz: 8fed8925c193caf3c169dff50800766d0abd9b45e6dd652de31b93b8219a2c3d425ac111a9c4b8608fbbfbc23a2e038f13bda2d89004a65fa801eb15dfcdbb37
|
data/.github/workflows/build.yml
CHANGED
@@ -11,15 +11,14 @@ jobs:
|
|
11
11
|
ruby: [2.5, 2.6, 2.7]
|
12
12
|
|
13
13
|
steps:
|
14
|
-
- uses: actions/checkout@
|
14
|
+
- uses: actions/checkout@v3
|
15
15
|
- name: Set up Ruby ${{ matrix.ruby }}
|
16
|
-
uses:
|
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.
|
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.
|
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.
|
data/lib/phi_attrs/configure.rb
CHANGED
@@ -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
|
data/lib/phi_attrs/logger.rb
CHANGED
@@ -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
|
data/lib/phi_attrs/phi_record.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
data/lib/phi_attrs/version.rb
CHANGED
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.
|
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:
|
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.
|
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
|