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 +4 -4
- data/README.md +56 -8
- data/lib/phi_attrs/configure.rb +5 -45
- data/lib/phi_attrs/exceptions.rb +2 -2
- data/lib/phi_attrs/formatter.rb +2 -2
- data/lib/phi_attrs/logger.rb +1 -1
- data/lib/phi_attrs/phi_record.rb +111 -79
- data/lib/phi_attrs/railtie.rb +5 -7
- data/lib/phi_attrs/rspec.rb +3 -3
- data/lib/phi_attrs/version.rb +1 -1
- data/lib/phi_attrs.rb +1 -0
- metadata +80 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 237044ea15d9e2cfbabb11218d60dbc32869ab91caa8c5f6f42f13eb2d9c34ff
|
|
4
|
+
data.tar.gz: 21b7e8149c94f9bc515767078bcf644976ebcda4e7c14209daec970b24ae8691
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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](
|
|
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
|
|
441
|
-
|
|
442
|
-
$
|
|
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
|
|
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
|
-
###
|
|
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
|
-
|
|
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).
|
data/lib/phi_attrs/configure.rb
CHANGED
|
@@ -1,53 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PhiAttrs
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
data/lib/phi_attrs/exceptions.rb
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
module PhiAttrs
|
|
4
4
|
module Exceptions
|
|
5
5
|
class PhiAccessException < StandardError
|
|
6
|
-
TAG =
|
|
6
|
+
TAG = "UNAUTHORIZED ACCESS"
|
|
7
7
|
|
|
8
8
|
def initialize(msg)
|
|
9
9
|
PhiAttrs::Logger.tagged(TAG) { PhiAttrs::Logger.error(msg) }
|
|
10
|
-
super
|
|
10
|
+
super
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
end
|
data/lib/phi_attrs/formatter.rb
CHANGED
|
@@ -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
|
data/lib/phi_attrs/logger.rb
CHANGED
data/lib/phi_attrs/phi_record.rb
CHANGED
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
557
|
-
@
|
|
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
|
-
|
|
580
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
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.
|
|
673
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
|
|
755
|
+
rel.class
|
|
724
756
|
end
|
|
725
757
|
|
|
726
758
|
def wrapped_extended_name(method_name)
|
data/lib/phi_attrs/railtie.rb
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "phi_attrs"
|
|
4
|
+
require "rails"
|
|
5
5
|
|
|
6
6
|
module PhiAttrs
|
|
7
7
|
class Railtie < Rails::Railtie
|
|
8
|
-
initializer
|
|
9
|
-
ActiveSupport.on_load(:active_record)
|
|
10
|
-
|
|
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
|
data/lib/phi_attrs/rspec.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "rspec/expectations"
|
|
4
4
|
|
|
5
|
-
DO_NOT_SPECIFY =
|
|
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 = [
|
|
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
|
|
data/lib/phi_attrs/version.rb
CHANGED
data/lib/phi_attrs.rb
CHANGED
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
101
|
+
version: '2.4'
|
|
69
102
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
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:
|
|
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:
|
|
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:
|
|
215
|
+
name: standardrb
|
|
155
216
|
requirement: !ruby/object:Gem::Requirement
|
|
156
217
|
requirements:
|
|
157
|
-
- - "
|
|
218
|
+
- - ">="
|
|
158
219
|
- !ruby/object:Gem::Version
|
|
159
|
-
version: '0
|
|
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
|
|
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:
|
|
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/
|
|
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.
|
|
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.
|
|
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: []
|