phi_attrs 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +30 -2
- data/lib/phi_attrs/phi_record.rb +153 -18
- data/lib/phi_attrs/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3eacb70390f2672da6d28043638c4c50e54c5097
|
4
|
+
data.tar.gz: cc5f1b55c52300d1588d168f94168cef75219590
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a3405b39cbe9006b92184db10ad4e31698512a5c742f43a201724042968e216ecde4977371f63d898a3143ca13e5bfcbbfe9cf7b47a53018d2103703d3ec1a4
|
7
|
+
data.tar.gz: b32c79d5304fe6c8bcecc94939b62b1d0d97cbae17db0aa3bc75dc5b47da01bd450f31e8a35113d18ec2542d4f047ca4e6e3aa73ad745b7f82f01450e92860b2
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# PhiAttrs
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/phi_attrs.svg)](https://badge.fury.io/rb/phi_attrs) [![Build Status](https://travis-ci.org/apsislabs/phi_attrs.svg?branch=master)](https://travis-ci.org/apsislabs/phi_attrs)
|
4
|
+
|
3
5
|
## Installation
|
4
6
|
|
5
7
|
Add this line to your application's Gemfile:
|
@@ -32,16 +34,42 @@ end
|
|
32
34
|
```
|
33
35
|
|
34
36
|
Access is granted on a model level:
|
37
|
+
|
35
38
|
```ruby
|
36
39
|
info = new PatientInfo
|
37
40
|
info.allow_phi!("allowed_user@example.com", "Customer Service")
|
38
41
|
```
|
39
42
|
|
40
43
|
or a class:
|
44
|
+
|
41
45
|
```ruby
|
42
46
|
PatientInfo.allow_phi!("allowed_user@example.com", "Customer Service")
|
43
47
|
```
|
44
48
|
|
49
|
+
### Extending PHI Access
|
50
|
+
|
51
|
+
Sometimes you'll have a single mental model that is composed of several `ActiveRecord` models joined by association. In this case, instead of calling `allow_phi!` on all joined models, we expose a shorthand of extending PHI access to related models.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class PatientInfo < ActiveRecord::Base
|
55
|
+
phi_model
|
56
|
+
end
|
57
|
+
|
58
|
+
class Patient < ActiveRecord::Base
|
59
|
+
has_one :patient_info
|
60
|
+
|
61
|
+
phi_model
|
62
|
+
|
63
|
+
extend_phi_access :patient_info
|
64
|
+
end
|
65
|
+
|
66
|
+
patient = Patient.new
|
67
|
+
patient.allow_phi!('user@example.com', 'reason')
|
68
|
+
patient.patient_info.first_name
|
69
|
+
```
|
70
|
+
|
71
|
+
**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.
|
72
|
+
|
45
73
|
## Development
|
46
74
|
|
47
75
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -50,8 +78,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
50
78
|
|
51
79
|
### Docker
|
52
80
|
|
53
|
-
|
54
|
-
|
81
|
+
- `docker-compose up`
|
82
|
+
- `bin/ssh_to_container`
|
55
83
|
|
56
84
|
## Testing
|
57
85
|
|
data/lib/phi_attrs/phi_record.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
+
# Namespace for classes and modules that handle PHI Attribute Access Logging
|
1
2
|
module PhiAttrs
|
2
3
|
PHI_ACCESS_LOG_TAG = 'PHI Access Log'.freeze
|
3
4
|
|
5
|
+
# Module for extending ActiveRecord models to handle PHI access logging
|
6
|
+
# and restrict access to attributes.
|
7
|
+
#
|
8
|
+
# @author Apsis Labs
|
9
|
+
# @since 0.1.0
|
4
10
|
module PhiRecord
|
5
11
|
extend ActiveSupport::Concern
|
6
12
|
|
@@ -17,18 +23,51 @@ module PhiAttrs
|
|
17
23
|
end
|
18
24
|
|
19
25
|
class_methods do
|
26
|
+
# Set methods to be excluded from PHI access logging.
|
27
|
+
#
|
28
|
+
# @param [Array<Symbol>] *methods Any number of methods to exclude
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# exclude_from_phi :foo, :bar
|
32
|
+
#
|
20
33
|
def exclude_from_phi(*methods)
|
21
34
|
self.__phi_exclude_methods = methods.map(&:to_s)
|
22
35
|
end
|
23
36
|
|
37
|
+
# Set methods to be explicitly included in PHI access logging.
|
38
|
+
#
|
39
|
+
# @param [Array<Symbol>] *methods Any number of methods to include
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# include_in_phi :foo, :bar
|
43
|
+
#
|
24
44
|
def include_in_phi(*methods)
|
25
45
|
self.__phi_include_methods = methods.map(&:to_s)
|
26
46
|
end
|
27
47
|
|
48
|
+
# Set of methods which should be implicitly allowed if this object
|
49
|
+
# is allowed. The methods that are extended should return ActiveRecord
|
50
|
+
# models that also extend PhiAttrs.
|
51
|
+
#
|
52
|
+
# If they do not, this is essentially an alias for PhiRecord#include_in_phi
|
53
|
+
#
|
54
|
+
# @param [Array<Symbol>] *methods Any number of methods to extend access to
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# extend_phi_access :foo, :bar
|
58
|
+
#
|
28
59
|
def extend_phi_access(*methods)
|
29
60
|
self.__phi_extended_methods = methods.map(&:to_s)
|
30
61
|
end
|
31
62
|
|
63
|
+
# Enable PHI access for any instance of this class.
|
64
|
+
#
|
65
|
+
# @param [String] user_id A unique identifier for the person accessing the PHI
|
66
|
+
# @param [String] reason The reason for accessing PHI
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# Foo.allow_phi!('user@example.com', 'viewing patient record')
|
70
|
+
#
|
32
71
|
def allow_phi!(user_id, reason)
|
33
72
|
RequestStore.store[:phi_access] ||= {}
|
34
73
|
|
@@ -37,11 +76,17 @@ module PhiAttrs
|
|
37
76
|
user_id: user_id,
|
38
77
|
reason: reason
|
39
78
|
}
|
79
|
+
|
40
80
|
PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do
|
41
81
|
PhiAttrs::Logger.info("PHI Access Enabled for #{user_id}: #{reason}")
|
42
82
|
end
|
43
83
|
end
|
44
84
|
|
85
|
+
# Revoke PHI access for this class, if enabled by PhiRecord#allow_phi!
|
86
|
+
#
|
87
|
+
# @example
|
88
|
+
# Foo.disallow_phi!
|
89
|
+
#
|
45
90
|
def disallow_phi!
|
46
91
|
RequestStore.store[:phi_access].delete(name) if RequestStore.store[:phi_access].present?
|
47
92
|
PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do
|
@@ -50,22 +95,27 @@ module PhiAttrs
|
|
50
95
|
end
|
51
96
|
end
|
52
97
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
# Wrap attributes with PHI Logger and Access Control
|
59
|
-
__phi_wrapped_methods.each { |attr| phi_wrap_method(attr) }
|
60
|
-
end
|
61
|
-
|
98
|
+
# Get all method names to be wrapped with PHI access logging
|
99
|
+
#
|
100
|
+
# @return [Array<String>] the method names to be wrapped with PHI access logging
|
101
|
+
#
|
62
102
|
def __phi_wrapped_methods
|
63
|
-
|
103
|
+
extended_methods = self.class.__phi_extended_methods.to_a
|
64
104
|
excluded_methods = self.class.__phi_exclude_methods.to_a
|
65
105
|
included_methods = self.class.__phi_include_methods.to_a
|
66
|
-
|
106
|
+
|
107
|
+
extended_methods + attribute_names - excluded_methods + included_methods - [self.class.primary_key]
|
67
108
|
end
|
68
109
|
|
110
|
+
# Enable PHI access for a single instance of this class.
|
111
|
+
#
|
112
|
+
# @param [String] user_id A unique identifier for the person accessing the PHI
|
113
|
+
# @param [String] reason The reason for accessing PHI
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# foo = Foo.find(1)
|
117
|
+
# foo.allow_phi!('user@example.com', 'viewing patient record')
|
118
|
+
#
|
69
119
|
def allow_phi!(user_id, reason)
|
70
120
|
PhiAttrs::Logger.tagged(*phi_log_keys) do
|
71
121
|
@__phi_access_allowed = true
|
@@ -76,6 +126,12 @@ module PhiAttrs
|
|
76
126
|
end
|
77
127
|
end
|
78
128
|
|
129
|
+
# Revoke PHI access for a single instance of this class
|
130
|
+
#
|
131
|
+
# @example
|
132
|
+
# foo = Foo.find(1)
|
133
|
+
# foo.disallow_phi!
|
134
|
+
#
|
79
135
|
def disallow_phi!
|
80
136
|
PhiAttrs::Logger.tagged(*phi_log_keys) do
|
81
137
|
@__phi_access_allowed = false
|
@@ -86,25 +142,104 @@ module PhiAttrs
|
|
86
142
|
end
|
87
143
|
end
|
88
144
|
|
145
|
+
# Whether PHI access is allowed for a single instance of this class
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# foo = Foo.find(1)
|
149
|
+
# foo.phi_allowed?
|
150
|
+
#
|
151
|
+
# @return [Boolean] whether PHI access is allowed for this instance
|
152
|
+
#
|
89
153
|
def phi_allowed?
|
90
154
|
@__phi_access_allowed || RequestStore.store.dig(:phi_access, self.class.name, :phi_access_allowed)
|
91
155
|
end
|
92
156
|
|
93
|
-
|
94
|
-
@__phi_user_id || RequestStore.store.dig(:phi_access, self.class.name, :user_id)
|
95
|
-
end
|
157
|
+
private
|
96
158
|
|
97
|
-
|
98
|
-
|
99
|
-
|
159
|
+
# Entry point for wrapping methods with PHI access logging. This is called
|
160
|
+
# by an `after_initialize` hook from ActiveRecord.
|
161
|
+
#
|
162
|
+
# @private
|
163
|
+
#
|
164
|
+
def wrap_phi
|
165
|
+
# Disable PHI access by default
|
166
|
+
@__phi_access_allowed = false
|
167
|
+
@__phi_access_logged = false
|
100
168
|
|
101
|
-
|
169
|
+
# Wrap attributes with PHI Logger and Access Control
|
170
|
+
__phi_wrapped_methods.each { |attr| phi_wrap_method(attr) }
|
171
|
+
end
|
102
172
|
|
173
|
+
# Log Key for an instance of this class. If the instance is persisted in the
|
174
|
+
# database, then it is the primary key; otherwise it is the Ruby object_id
|
175
|
+
# in memory.
|
176
|
+
#
|
177
|
+
# This is used by the tagged logger for tagging all log entries to find
|
178
|
+
# the underlying model.
|
179
|
+
#
|
180
|
+
# @private
|
181
|
+
#
|
182
|
+
# @return [Array<String>] log key for an instance of this class
|
183
|
+
#
|
103
184
|
def phi_log_keys
|
104
185
|
@__phi_log_id = persisted? ? "Key: #{attributes[self.class.primary_key]}" : "Object: #{object_id}"
|
105
186
|
@__phi_log_keys = [PHI_ACCESS_LOG_TAG, self.class.name, @__phi_log_id]
|
106
187
|
end
|
107
188
|
|
189
|
+
# The unique identifier for whom access has been allowed on this instance.
|
190
|
+
# This is what was passed in when PhiRecord#allow_phi! was called.
|
191
|
+
#
|
192
|
+
# @private
|
193
|
+
#
|
194
|
+
# @return [String] the user_id passed in to allow_phi!
|
195
|
+
#
|
196
|
+
def phi_allowed_by
|
197
|
+
@__phi_user_id || RequestStore.store.dig(:phi_access, self.class.name, :user_id)
|
198
|
+
end
|
199
|
+
|
200
|
+
# The access reason for allowing access to this instance.
|
201
|
+
# This is what was passed in when PhiRecord#allow_phi! was called.
|
202
|
+
#
|
203
|
+
# @private
|
204
|
+
#
|
205
|
+
# @return [String] the reason passed in to allow_phi!
|
206
|
+
#
|
207
|
+
def phi_access_reason
|
208
|
+
@__phi_access_reason || RequestStore.store.dig(:phi_access, self.class.name, :reason)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Core logic for wrapping methods in PHI access logging and access restriction.
|
212
|
+
#
|
213
|
+
# This method takes a single method name, and creates a new method using
|
214
|
+
# define_method; once this method is defined, the original method name
|
215
|
+
# is aliased to the new method, and the original method is renamed to a
|
216
|
+
# known key.
|
217
|
+
#
|
218
|
+
# @private
|
219
|
+
#
|
220
|
+
# @example
|
221
|
+
# Foo::phi_wrap_method(:bar)
|
222
|
+
#
|
223
|
+
# foo = Foo.find(1)
|
224
|
+
# foo.bar # => raises PHI Access Exception
|
225
|
+
#
|
226
|
+
# foo.allow_phi!('user@example.com', 'testing')
|
227
|
+
#
|
228
|
+
# foo.bar # => returns original value of Foo#bar
|
229
|
+
#
|
230
|
+
# # defines two new methods:
|
231
|
+
# # __bar_phi_wrapped
|
232
|
+
# # __bar_phi_unwrapped
|
233
|
+
# #
|
234
|
+
# # After these methods are defined
|
235
|
+
# # an alias chain is created that
|
236
|
+
# # roughly maps:
|
237
|
+
# #
|
238
|
+
# # bar => __bar_phi_wrapped => __bar_phi_unwrapped
|
239
|
+
# #
|
240
|
+
# # This ensures that all calls to Foo#bar pass
|
241
|
+
# # through access logging.
|
242
|
+
#
|
108
243
|
def phi_wrap_method(method_name)
|
109
244
|
return if self.class.__phi_methods_wrapped.include? method_name
|
110
245
|
|
data/lib/phi_attrs/version.rb
CHANGED