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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 07c9497b2d8b7c17ad211cd9bd9ed40fdf669e78
4
- data.tar.gz: 3e42b4499d9b45d1f46a52d325869f171becb27d
3
+ metadata.gz: 3eacb70390f2672da6d28043638c4c50e54c5097
4
+ data.tar.gz: cc5f1b55c52300d1588d168f94168cef75219590
5
5
  SHA512:
6
- metadata.gz: 7f4e4acdcc682ba9b0cca43644f39a248ea3878e2138d8c6105246c74d9df6c1ad1c304cec13a790398c7430b70b140f56d60e701e973c374a9dfc250ef65a3c
7
- data.tar.gz: 67737de996af40fa8ceea1b68a69f2ce516e7035c02d0e70183e8b56058e2a17ea2e6d5fb2030cf9df25a0e688072e1cc871c0b41ae365544f50132cf6a26bd0
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
- * `docker-compose up`
54
- * `bin/ssh_to_container`
81
+ - `docker-compose up`
82
+ - `bin/ssh_to_container`
55
83
 
56
84
  ## Testing
57
85
 
@@ -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
- def wrap_phi
54
- # Disable PHI access by default
55
- @__phi_access_allowed = false
56
- @__phi_access_logged = false
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
- associations = self.class.reflect_on_all_associations.map(&:name).map(&:to_s)
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
- associations + attribute_names - excluded_methods + included_methods - [self.class.primary_key]
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
- def phi_allowed_by
94
- @__phi_user_id || RequestStore.store.dig(:phi_access, self.class.name, :user_id)
95
- end
157
+ private
96
158
 
97
- def phi_access_reason
98
- @__phi_access_reason || RequestStore.store.dig(:phi_access, self.class.name, :reason)
99
- end
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
- private
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
 
@@ -1,3 +1,3 @@
1
1
  module PhiAttrs
2
- VERSION = '0.1.2'.freeze
2
+ VERSION = '0.1.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phi_attrs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wyatt Kirby