Skip to content

Commit 33d7a62

Browse files
committed
Add constraints for feature report routes
Rather than allowing any method on FeatureReportService to be called (and potentially opening up a security hole), this commit adds some metaprogramming machinery to manage a list of methods which are feature reports and adds methods to allow that to be used safely, both to constrain the parameters in the routes and to prevent arbitrary methods from being called from the controller.
1 parent 3af73bf commit 33d7a62

5 files changed

Lines changed: 86 additions & 9 deletions

File tree

app/controllers/reports_controller.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ def questions_with_answer_type
2222
def feature_report
2323
report = params[:report].underscore
2424

25-
raise ActionController::RoutingError unless Reports::FeatureReportService.method_defined? report
26-
2725
forms = Reports::FormDocumentsService.live_form_documents
28-
records = Reports::FeatureReportService.new(forms).send report
26+
records = Reports::FeatureReportService.new(forms).report report
2927

3028
if params[:format] == "csv"
3129
send_data Reports::CsvReportService.new(records).csv,

app/services/reports/feature_report_service.rb

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
11
class Reports::FeatureReportService
2+
def self.define_report(name, &block)
3+
@reports ||= []
4+
@reports << name
5+
6+
define_method(name, &block)
7+
end
8+
9+
def self.matches_report?(name)
10+
@reports.include? name.to_sym
11+
end
12+
13+
class Constraint
14+
def self.matches?(request)
15+
name = request.params[:report].underscore
16+
Reports::FeatureReportService.matches_report? name
17+
end
18+
end
19+
20+
private_class_method :define_report
21+
222
attr_reader :form_documents
323

424
def initialize(form_documents)
525
@form_documents = form_documents
626
end
727

8-
def report
28+
def report(name = nil)
29+
unless name.nil?
30+
raise "'#{name}' is not a defined report" unless self.class.matches_report?(name)
31+
32+
return send(name)
33+
end
34+
935
report = {
1036
total_forms: 0,
1137
forms_with_payment: 0,
@@ -54,27 +80,27 @@ def questions_with_answer_type(answer_type)
5480
end
5581
end
5682

57-
def questions_with_add_another_answer
83+
define_report :questions_with_add_another_answer do
5884
form_documents.flat_map do |form|
5985
form["content"]["steps"]
6086
.select { |step| step["data"]["is_repeatable"] }
6187
.map { |step| questions_details(form, step) }
6288
end
6389
end
6490

65-
def forms_with_routes
91+
define_report :forms_with_routes do
6692
form_documents
6793
.select { |form| Reports::FormDocumentsService.has_routes?(form) }
6894
.map { |form| form_with_routes_details(form) }
6995
end
7096

71-
def forms_with_payments
97+
define_report :forms_with_payments do
7298
form_documents
7399
.select { |form| Reports::FormDocumentsService.has_payments?(form) }
74100
.map { |form| form_details(form) }
75101
end
76102

77-
def forms_with_csv_submission_enabled
103+
define_report :forms_with_csv_submission_enabled do
78104
form_documents
79105
.select { |form| Reports::FormDocumentsService.has_csv_submission_enabled?(form) }
80106
.map { |form| form_details(form) }

config/routes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@
196196
scope "/features" do
197197
get "/", to: "reports#features", as: :report_features
198198
get "/questions-with-answer-type/:answer_type", to: "reports#questions_with_answer_type", as: :report_questions_with_answer_type
199-
get "/:report", to: "reports#feature_report", as: :feature_report
199+
get "/:report", constraints: Reports::FeatureReportService::Constraint, to: "reports#feature_report", as: :feature_report
200200
end
201201

202202
get "users", to: "reports#users", as: :report_users

spec/routing/reports_routing_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
it "routes to #feature_report with forms_with_csv_submission_enabled and csv format" do
4343
expect(get: "/reports/features/forms-with-csv-submission-enabled.csv").to route_to("reports#feature_report", report: "forms-with-csv-submission-enabled", format: "csv")
4444
end
45+
46+
it "does not route to #feature_report if param does not match defined report" do
47+
expect(get: "/reports/features/foobar").not_to route_to("reports#feature_report", report: "foobar")
48+
end
4549
end
4650

4751
describe "path helpers" do

spec/services/reports/feature_report_service_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,32 @@
1111
GroupForm.create!(form_id: 4, group:)
1212
end
1313

14+
describe "Constraint" do
15+
it "returns true if given the name of a defined report in slug format" do
16+
expect(described_class::Constraint.matches?(OpenStruct.new(params: { report: "forms-with-routes" }))).to be true
17+
expect(described_class::Constraint.matches?(OpenStruct.new(params: { report: "questions-with-add-another-answer" }))).to be true
18+
end
19+
20+
it "returns false if names does not match a defined report" do
21+
expect(described_class.matches_report?(OpenStruct.new(params: { report: "report" }))).to be false
22+
expect(described_class.matches_report?(OpenStruct.new(params: { report: "inspect" }))).to be false
23+
expect(described_class.matches_report?(OpenStruct.new(params: { report: "display" }))).to be false
24+
end
25+
end
26+
27+
describe ".matches_report?" do
28+
it "returns true if given the name of a defined report" do
29+
expect(described_class.matches_report?(:forms_with_routes)).to be true
30+
expect(described_class.matches_report?(:questions_with_add_another_answer)).to be true
31+
end
32+
33+
it "returns false if names does not match a defined report" do
34+
expect(described_class.matches_report?(:report)).to be false
35+
expect(described_class.matches_report?(:inspect)).to be false
36+
expect(described_class.matches_report?(:display)).to be false
37+
end
38+
end
39+
1440
describe "#report" do
1541
it "returns the feature report" do
1642
report = described_class.new(form_documents).report
@@ -44,6 +70,29 @@
4470
},
4571
})
4672
end
73+
74+
context "with the name of a feature report" do
75+
it "returns that report" do
76+
report = []
77+
feature_report_service = described_class.new(form_documents)
78+
allow(feature_report_service).to receive(:forms_with_routes).and_return report
79+
80+
expect(feature_report_service.report(:forms_with_routes)).to be report
81+
82+
expect(feature_report_service).to have_received(:forms_with_routes)
83+
end
84+
end
85+
86+
context "with the name of a method that is not a feature report" do
87+
it "raises an error" do
88+
feature_report_service = described_class.new(form_documents)
89+
allow(feature_report_service).to receive(:display)
90+
91+
expect { feature_report_service.report(:display) }.to raise_error(/not a defined report/)
92+
93+
expect(feature_report_service).not_to have_received(:display)
94+
end
95+
end
4796
end
4897

4998
describe "#questions" do

0 commit comments

Comments
 (0)