Skip to content

Commit 61d8df6

Browse files
committed
Disallow ops users from viewing PII for restricted patients
1 parent 2533322 commit 61d8df6

11 files changed

Lines changed: 376 additions & 300 deletions

File tree

app/components/app_timeline_component.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def format_description(item)
6363
"#{id_info}#{formatted_details}"
6464
elsif item[:description].present?
6565
item[:description]
66+
else
67+
""
6668
end
6769
end
6870

app/controllers/inspect/graphs_controller.rb

Lines changed: 94 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,104 +14,137 @@ class GraphsController < ApplicationController
1414

1515
def show
1616
authorize :inspect, :graph?
17-
@primary_type = safe_get_primary_type
18-
if @primary_type.nil?
17+
if primary_type.nil?
1918
render plain:
2019
"You don't have permission to view object type: #{params[:object_type].to_s.downcase.singularize}",
2120
status: :bad_request and return
2221
end
23-
@primary_id = params[:object_id]
2422

2523
# Set default relationships when loading a page for the first time
2624
if params[:relationships].blank? &&
27-
GraphRecords::DEFAULT_TRAVERSALS.key?(@primary_type)
28-
default_rels = GraphRecords::DEFAULT_TRAVERSALS[@primary_type] || {}
25+
GraphRecords::DEFAULT_TRAVERSALS.key?(primary_type)
26+
default_rels = GraphRecords::DEFAULT_TRAVERSALS[primary_type] || {}
2927

3028
new_params = params.to_unsafe_h.merge("relationships" => default_rels)
3129
redirect_to inspect_path(new_params) and return
3230
end
3331

34-
@object = @primary_type.to_s.classify.constantize.find(@primary_id)
35-
36-
@traversals_config = build_traversals_config
37-
@graph_params = build_graph_params
38-
set_pii_settings
39-
4032
@graph_record =
4133
GraphRecords.new(
42-
traversals_config: @traversals_config,
43-
primary_type: @primary_type,
34+
traversals_config:,
35+
primary_type:,
4436
clickable: true,
45-
show_pii: @show_pii
37+
show_pii:
4638
)
47-
@mermaid = @graph_record.graph(**@graph_params).join("\n")
39+
@mermaid = @graph_record.graph(**graph_params).join("\n")
4840
end
4941

5042
private
5143

52-
def set_pii_settings
53-
@user_is_allowed_to_access_pii = policy(:inspect).show_pii?
54-
@show_pii =
55-
@user_is_allowed_to_access_pii &&
56-
(params[:show_pii] || SHOW_PII_BY_DEFAULT)
44+
def show_pii
45+
@show_pii ||=
46+
user_is_allowed_to_access_pii && !sensitive_patient_in_graph &&
47+
pii_access_requested_by_user
5748
end
5849

59-
def build_traversals_config
60-
traversals_config = {}
61-
to_process = Set.new([@primary_type])
62-
processed = Set.new
50+
def user_is_allowed_to_access_pii
51+
@user_is_allowed_to_access_pii ||= policy(:inspect).show_pii?
52+
end
6353

64-
# Process types until we've explored all connected relationships
65-
while (type = to_process.first)
66-
to_process.delete(type)
67-
processed.add(type)
54+
def sensitive_patient_in_graph
55+
@sensitive_patient_in_graph ||=
56+
begin
57+
graph_with_pii =
58+
GraphRecords.new(
59+
traversals_config:,
60+
primary_type:,
61+
clickable: true,
62+
show_pii: true
63+
)
64+
graph_with_pii.graph(**graph_params)
65+
graph_with_pii.patients_with_pii_in_graph.any?(&:restricted?)
66+
end
67+
end
6868

69-
selected_rels =
70-
Array(params.dig(:relationships, type)).reject(&:blank?).map(&:to_sym)
69+
def pii_access_requested_by_user
70+
@pii_access_requested_by_user ||= params[:show_pii] || SHOW_PII_BY_DEFAULT
71+
end
7172

72-
traversals_config[type] = selected_rels
73+
def traversals_config
74+
@traversals_config ||=
75+
begin
76+
traversals = {}
77+
to_process = Set.new([primary_type])
78+
processed = Set.new
79+
80+
# Process types until we've explored all connected relationships
81+
while (type = to_process.first)
82+
to_process.delete(type)
83+
processed.add(type)
84+
85+
selected_rels =
86+
Array(params.dig(:relationships, type)).reject(&:blank?).map(
87+
&:to_sym
88+
)
7389

74-
# Add target types to process queue
75-
klass = type.to_s.classify.constantize
76-
selected_rels.each do |rel|
77-
association = klass.reflect_on_association(rel)
78-
next unless association
90+
traversals[type] = selected_rels
7991

80-
target_type = association.klass.name.underscore.to_sym
81-
to_process.add(target_type) unless processed.include?(target_type)
82-
end
83-
end
92+
# Add target types to process queue
93+
klass = type.to_s.classify.constantize
94+
selected_rels.each do |rel|
95+
association = klass.reflect_on_association(rel)
96+
next unless association
8497

85-
traversals_config
98+
target_type = association.klass.name.underscore.to_sym
99+
to_process.add(target_type) unless processed.include?(target_type)
100+
end
101+
end
102+
103+
traversals
104+
end
86105
end
87106

88-
def build_graph_params
89-
graph_params = { @primary_type => [@object.id] }
90-
91-
if params[:additional_ids].present?
92-
params[:additional_ids].each do |type, ids_string|
93-
next if ids_string.blank?
94-
additional_ids = ids_string.split(",").map { |s| s.strip.to_i }
95-
next unless additional_ids.any?
96-
type_sym = type.to_sym
97-
graph_params[type_sym] ||= []
98-
graph_params[type_sym].concat(additional_ids)
107+
def graph_params
108+
@graph_params ||=
109+
begin
110+
graph_params = { primary_type => [primary_object.id] }
111+
112+
if params[:additional_ids].present?
113+
params[:additional_ids].each do |type, ids_string|
114+
next if ids_string.blank?
115+
additional_ids = ids_string.split(",").map { |s| s.strip.to_i }
116+
next unless additional_ids.any?
117+
type_sym = type.to_sym
118+
graph_params[type_sym] ||= []
119+
graph_params[type_sym].concat(additional_ids)
120+
end
121+
end
122+
123+
graph_params
99124
end
100-
end
125+
end
101126

102-
graph_params
127+
def primary_type
128+
@primary_type ||=
129+
begin
130+
singular_type = params[:object_type].downcase.singularize
131+
return nil unless GraphRecords::ALLOWED_TYPES.include?(singular_type)
132+
singular_type.to_sym
133+
end
103134
end
104135

105-
def safe_get_primary_type
106-
singular_type = params[:object_type].downcase.singularize
107-
return nil unless GraphRecords::ALLOWED_TYPES.include?(singular_type)
108-
singular_type.to_sym
136+
def primary_object
137+
@primary_object ||=
138+
begin
139+
@primary_id = params[:object_id]
140+
primary_type.to_s.classify.constantize.find(@primary_id)
141+
end
109142
end
110143

111144
def pii_accessed?
112-
return false unless @show_pii
145+
return false unless show_pii
113146

114-
build_traversals_config.any? do |from_type, rels|
147+
traversals_config.any? do |from_type, rels|
115148
GraphRecords::EXTRA_DETAIL_WHITELIST_WITH_PII.key?(
116149
from_type.name.underscore.to_sym
117150
) ||
@@ -142,7 +175,7 @@ def record_access_log_entry
142175
controller: "graph",
143176
action: "show_pii",
144177
request_details: {
145-
primary_type: @primary_type,
178+
primary_type:,
146179
primary_id: @primary_id,
147180
additional_ids: additional_ids.presence,
148181
visible_fields: request_details

app/controllers/inspect/timeline/patients_controller.rb

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,9 @@ class Inspect::Timeline::PatientsController < ApplicationController
1313

1414
def show
1515
authorize :inspect, :timeline?
16-
set_pii_settings
1716

1817
params[:audit_config] ||= {}
1918

20-
compare_option = params[:compare_option] || nil
21-
2219
# Set default values if none present
2320
if params[:detail_config].nil? && params[:event_names].nil? &&
2421
params[:show_pii].nil?
@@ -40,17 +37,16 @@ def show
4037
params[:audit_config][:include_filtered_audit_changes]
4138
}
4239

43-
@compare_patient = sample_patient(params[:compare_option]) if compare_option
4440
@event_names = params[:event_names] || []
4541

4642
record_access_log_entry
4743

4844
@patient_timeline =
4945
TimelineRecords.new(
5046
@patient,
51-
detail_config: build_details_config,
52-
audit_config: audit_config,
53-
show_pii: @show_pii
47+
details_config:,
48+
audit_config:,
49+
show_pii:
5450
).load_timeline_events(@event_names)
5551

5652
@no_events_message = true if @patient_timeline.empty?
@@ -61,9 +57,9 @@ def show
6157
@compare_patient_timeline =
6258
TimelineRecords.new(
6359
@compare_patient,
64-
detail_config: build_details_config,
65-
audit_config: audit_config,
66-
show_pii: @show_pii
60+
details_config:,
61+
audit_config:,
62+
show_pii:
6763
).load_timeline_events(@event_names)
6864

6965
@no_events_compare_message = true if @compare_patient_timeline.empty?
@@ -72,18 +68,33 @@ def show
7268

7369
private
7470

75-
def set_pii_settings
76-
@user_is_allowed_to_access_pii = policy(:inspect).show_pii?
77-
@show_pii =
78-
@user_is_allowed_to_access_pii &&
79-
(params[:show_pii] || SHOW_PII_BY_DEFAULT)
71+
def show_pii
72+
@show_pii ||=
73+
user_is_allowed_to_access_pii && !patient_accessed_is_sensitive &&
74+
pii_access_requested_by_user
75+
end
76+
77+
def user_is_allowed_to_access_pii
78+
@user_is_allowed_to_access_pii ||= policy(:inspect).show_pii?
79+
end
80+
81+
def patient_accessed_is_sensitive
82+
@patient_accessed_is_sensitive ||=
83+
@patient.restricted? || @compare_patient&.restricted?
84+
end
85+
86+
def pii_access_requested_by_user
87+
@pii_access_requested_by_user ||= params[:show_pii] || SHOW_PII_BY_DEFAULT
8088
end
8189

8290
def set_patient
8391
@patient = Patient.find(params[:id])
8492
timeline = TimelineRecords.new(@patient)
8593
@patient_events = timeline.patient_events(@patient)
8694
@additional_events = timeline.additional_events(@patient)
95+
96+
compare_option = params[:compare_option] || nil
97+
@compare_patient = sample_patient(params[:compare_option]) if compare_option
8798
end
8899

89100
# TODO: Fix so that a new comparison patient isn't sampled every time
@@ -118,54 +129,57 @@ def sample_patient(compare_option)
118129
end
119130
end
120131

121-
def build_details_config
122-
details_params = params[:detail_config] || {}
123-
details_params = details_params.to_unsafe_h unless details_params.is_a?(
124-
Hash
125-
)
126-
127-
event_list_details =
128-
(@event_names - ["audits"]).map { [it.to_sym, []] }.to_h
129-
user_submitted_details =
130-
details_params.each_with_object(
131-
event_list_details
132-
) do |(event_type, fields), hash|
133-
selected_fields = Array(fields).reject(&:blank?).map(&:to_sym)
134-
hash[event_type.to_sym] = selected_fields
135-
end
132+
def details_config
133+
@details_config ||=
134+
begin
135+
details_params = params[:detail_config] || {}
136+
details_params = details_params.to_unsafe_h unless details_params.is_a?(
137+
Hash
138+
)
136139

137-
details_mask =
138-
(
139-
if @show_pii
140-
TimelineRecords::AVAILABLE_DETAILS_CONFIG_WITH_PII
141-
else
142-
TimelineRecords::AVAILABLE_DETAILS_CONFIG
140+
event_list_details =
141+
(@event_names - ["audits"]).map { [it.to_sym, []] }.to_h
142+
user_submitted_details =
143+
details_params.each_with_object(
144+
event_list_details
145+
) do |(event_type, fields), hash|
146+
selected_fields = Array(fields).reject(&:blank?).map(&:to_sym)
147+
hash[event_type.to_sym] = selected_fields
148+
end
149+
150+
# Removes details from the config which the user does not have permission to view
151+
details_mask =
152+
(
153+
if show_pii
154+
TimelineRecords::AVAILABLE_DETAILS_CONFIG_WITH_PII
155+
else
156+
TimelineRecords::AVAILABLE_DETAILS_CONFIG
157+
end
158+
)
159+
(details_mask.keys & user_submitted_details.keys).index_with do |key|
160+
details_mask[key] & user_submitted_details[key]
143161
end
144-
)
145-
(details_mask.keys & user_submitted_details.keys).index_with do |key|
146-
details_mask[key] & user_submitted_details[key]
147-
end
162+
end
148163
end
149164

150165
def pii_accessed?
151-
return false unless @show_pii
152-
detail_config = build_details_config
153-
detail_config.any? do |event_type, selected_fields|
166+
return false unless show_pii
167+
details_config.any? do |event_type, selected_fields|
154168
pii_fields =
155169
TimelineRecords::AVAILABLE_DETAILS_CONFIG_PII[event_type] || []
156170
(selected_fields & pii_fields).any?
157171
end
158172
end
159173

160174
def audit_pii_accessed?
161-
true if @show_pii && @event_names.include?("audits")
175+
true if show_pii && @event_names.include?("audits")
162176
end
163177

164178
def record_access_log_entry
165179
return unless pii_accessed? || audit_pii_accessed?
166180

167181
details_accessed =
168-
build_details_config.reverse_merge(
182+
details_config.reverse_merge(
169183
@event_names.map { |key| [key.to_sym, []] }.to_h
170184
)
171185
details_accessed[:audits] = :accessed if details_accessed.key?(:audits)

0 commit comments

Comments
 (0)