Skip to content

Commit b9d5bcc

Browse files
authored
Disallow PII access to restricted patients in ops tools
This disallows PII access to restricted patients for ops users, and fixes some small bugs related to the ops tools: - Fix bug where user selects no parameters for an event type so it reverts to the default - Fix bug where a non-PII user could view PII by modifying the URL Jira-Issue: MAV-3019
2 parents 51bf616 + 61d8df6 commit b9d5bcc

11 files changed

Lines changed: 377 additions & 284 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: 60 additions & 29 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,37 +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-
)
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+
)
126139

127-
details_params.each_with_object({}) do |(event_type, fields), hash|
128-
selected_fields = Array(fields).reject(&:blank?).map(&:to_sym)
129-
hash[event_type.to_sym] = selected_fields
130-
end
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]
161+
end
162+
end
131163
end
132164

133165
def pii_accessed?
134-
return false unless @show_pii
135-
detail_config = build_details_config
136-
detail_config.any? do |event_type, selected_fields|
166+
return false unless show_pii
167+
details_config.any? do |event_type, selected_fields|
137168
pii_fields =
138169
TimelineRecords::AVAILABLE_DETAILS_CONFIG_PII[event_type] || []
139170
(selected_fields & pii_fields).any?
140171
end
141172
end
142173

143174
def audit_pii_accessed?
144-
true if @show_pii && @event_names.include?("audits")
175+
true if show_pii && @event_names.include?("audits")
145176
end
146177

147178
def record_access_log_entry
148179
return unless pii_accessed? || audit_pii_accessed?
149180

150181
details_accessed =
151-
build_details_config.reverse_merge(
182+
details_config.reverse_merge(
152183
@event_names.map { |key| [key.to_sym, []] }.to_h
153184
)
154185
details_accessed[:audits] = :accessed if details_accessed.key?(:audits)

0 commit comments

Comments
 (0)