Skip to content

Commit adb21fa

Browse files
Merge pull request #353 from MITLibraries/rest-api
Add protected ability to request JSON results with token
2 parents b4874c4 + 123a7e7 commit adb21fa

File tree

3 files changed

+87
-0
lines changed

3 files changed

+87
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ may have unexpected consequences if applied to other TIMDEX UI apps.
114114
- `FILTER_PLACE`: The name to use instead of "Place" for that filter / aggregation.
115115
- `FILTER_SOURCE`: The name to use instead of "Source" for that filter / aggregation.
116116
- `FILTER_SUBJECT`: The name to use instead of "Subject" for that filter / aggregation.
117+
- `FORMAT_TOKEN`: If defined, user agents can request results in JSON format by providing this value in the `format_token` querystring parameter.
117118
- `GLOBAL_ALERT`: The main functionality for this comes from our theme gem, but when set the value will be rendered as
118119
safe html above the main header of the site.
119120
- `THIRDIRON_KEY`: An access key assigned by Third Iron to enable this application to interact with the Libkey service.

app/controllers/search_controller.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
class SearchController < ApplicationController
22
before_action :validate_q!, only: %i[results]
3+
before_action :validate_format_token, only: %i[results]
34
before_action :set_active_tab, only: %i[results]
45
around_action :sleep_if_too_fast, only: %i[results]
56

@@ -33,6 +34,12 @@ def results
3334
when *timdex_tabs
3435
load_timdex_results
3536
end
37+
38+
# Render the response in HTML or JSON format
39+
respond_to do |format|
40+
format.json { render json: { results: @results, pagination: @pagination, errors: @errors } }
41+
format.html { render :results }
42+
end
3643
end
3744

3845
private
@@ -381,4 +388,31 @@ def handle_primo_errors(error)
381388
[{ 'message' => error.message }]
382389
end
383390
end
391+
392+
# validate_format_token is only applicable to requests for JSON-format results. It takes no action so long as the
393+
# valid_request_for_json? method returns true - otherwise it renders an error message with a 401 Unauthorized status.
394+
def validate_format_token
395+
return unless request.format.json?
396+
397+
return if valid_request_for_json?
398+
399+
render json: { error: 'Unauthorized request' }, status: :unauthorized
400+
end
401+
402+
# valid_request_for_json? is responsible for validating whether a request for JSON format results is accompanied by
403+
# a token which matches the value defined in env.
404+
# 1. If the ENV is undefined, then the feature is not enabled - the check fails, which will prompt an Unauthorized
405+
# error.
406+
# 2. If the ENV is defined, and the provided token matches, then the check fails, and the request will be honored.
407+
# 3. In all other cases, the check fails, which will prompt the Unauthorized error.
408+
def valid_request_for_json?
409+
# Always fail unless the token is defined in ENV
410+
return false unless ENV.fetch('FORMAT_TOKEN', '').present?
411+
412+
# Success if tokens match
413+
return true if params[:format_token] == ENV.fetch('FORMAT_TOKEN', '')
414+
415+
# Otherwise fail
416+
false
417+
end
384418
end

test/controllers/search_controller_test.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,4 +1113,56 @@ def source_filter_count(controller)
11131113
# Should show current range (21-40 for page 2)
11141114
assert_select '.pagination-container .current', text: /21 - 40 of 800/
11151115
end
1116+
1117+
test 'results can be returned in JSON format when env is set and valid token is provided' do
1118+
secret_value = 'sooper_sekret'
1119+
ClimateControl.modify FORMAT_TOKEN: secret_value do
1120+
mock_timdex_search_with_hits(10)
1121+
get "/results?q=test&format=json&format_token=#{secret_value}"
1122+
assert_response :success
1123+
assert_equal 'application/json; charset=utf-8', response.content_type
1124+
end
1125+
end
1126+
1127+
# We don't mock anything here because the error prevents any external lookups
1128+
test 'requests for JSON results without a token generate an unauthorized error' do
1129+
secret_value = 'sooper_sekret'
1130+
ClimateControl.modify FORMAT_TOKEN: secret_value do
1131+
get '/results?q=test&format=json'
1132+
assert_response :unauthorized
1133+
end
1134+
end
1135+
1136+
# We don't mock anything here because the error prevents any external lookups
1137+
test 'requests for JSON results when env var is not set generate an unauthorized error' do
1138+
secret_value = 'irrelevant'
1139+
ClimateControl.modify FORMAT_TOKEN: '' do
1140+
get "/results?q=test&format=json&format_token=#{secret_value}"
1141+
assert_response :unauthorized
1142+
end
1143+
end
1144+
1145+
# We don't mock anything here because the error prevents any external lookups
1146+
test 'requests for JSON results with an incorrect token generate an unauthorized error' do
1147+
secret_value = 'sooper_sekret'
1148+
wrong_secret = 'something_else'
1149+
refute_equal secret_value, wrong_secret
1150+
1151+
ClimateControl.modify FORMAT_TOKEN: secret_value do
1152+
get "/results?q=test&format=json&format_token=#{wrong_secret}"
1153+
assert_response :unauthorized
1154+
end
1155+
end
1156+
1157+
test 'requests with unsupported format receive a 406 not-acceptable error' do
1158+
mock_timdex_search_with_hits(10)
1159+
get '/results?q=test&format=xml'
1160+
assert_response :not_acceptable
1161+
end
1162+
1163+
test 'requests with invalid format receive a 406 not-acceptable error' do
1164+
mock_timdex_search_with_hits(10)
1165+
get '/results?q=test&format=foo'
1166+
assert_response :not_acceptable
1167+
end
11161168
end

0 commit comments

Comments
 (0)