Skip to content

Commit 4809318

Browse files
author
Alistair Davidson
committed
add /api/reporting controllers, plus supporting config - but make them only routable on non-production environments for now
1 parent 4ef36b4 commit 4809318

File tree

11 files changed

+335
-2
lines changed

11 files changed

+335
-2
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
class API::Reporting::BaseController < ActionController::API
4+
# we need to still include the AuthenticationConcern even though
5+
# we're not using the authenticate_user! callback, because we call it
6+
# explicitly after validating the users' JWT in order to use the
7+
# CIS2 organisation/workgroup validation code
8+
include AuthenticationConcern
9+
include ReportingAPI::TokenAuthenticationConcern
10+
11+
before_action :ensure_reporting_api_feature_enabled
12+
before_action :authenticate_user_by_jwt!
13+
14+
private
15+
16+
def ensure_reporting_api_feature_enabled
17+
render status: :forbidden and return unless Flipper.enabled?(:reporting_api)
18+
end
19+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
class API::Reporting::OneTimeTokensController < API::Reporting::BaseController
4+
# skip_before_action :authenticate_user!
5+
before_action :ensure_reporting_api_feature_enabled,
6+
:authenticate_app_by_client_id!,
7+
:verify_grant_type!
8+
9+
def authorize
10+
@token = ReportingAPI::OneTimeToken.find_by!(token: params[:code])
11+
@token.delete # <- Tokens are one-time use
12+
json_data = { jwt: jwt(@token) }
13+
render json: json_data
14+
rescue ActiveRecord::RecordNotFound
15+
render json: { errors: "invalid_grant" }, status: :forbidden
16+
end
17+
18+
private
19+
20+
def verify_grant_type!
21+
unless params["grant_type"] == "authorization_code"
22+
render json: { error: "unsupported_grant_type" }, status: :bad_request and
23+
return
24+
end
25+
end
26+
27+
def jwt_payload(token)
28+
{
29+
"iat" => Time.current.utc.to_i,
30+
"data" => {
31+
"user" => token.user.as_json,
32+
"cis2_info" => token.cis2_info
33+
}
34+
}
35+
end
36+
37+
def jwt(token)
38+
JWT.encode(
39+
jwt_payload(token),
40+
Settings.reporting_api.client_app.secret,
41+
"HS512"
42+
)
43+
end
44+
45+
def ensure_reporting_api_feature_enabled
46+
render status: :forbidden and return unless Flipper.enabled?(:reporting_api)
47+
end
48+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class API::Reporting::TotalsController < API::Reporting::BaseController
4+
def index
5+
render json: { total: "dummy data" }
6+
end
7+
end

app/controllers/concerns/authentication_concern.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,12 @@ def after_sign_in_path_for(scope)
111111
if Flipper.enabled?(:reporting_api)
112112
urls << reporting_app_redirect_uri_with_auth_code_for(current_user)
113113
end
114-
urls += [stored_location_for(scope), dashboard_path]
114+
urls += [
115+
stored_location_for(scope),
116+
session[:user_return_to],
117+
dashboard_path
118+
]
119+
115120
urls.compact.find do
116121
is_valid_redirect?(it) && (it != request.fullpath) &&
117122
(it != new_users_teams_path)

app/controllers/users/teams_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def create
3535
private
3636

3737
def return_to_path
38-
session[:user_return_to] || dashboard_path
38+
after_sign_in_path_for(current_user) || session[:user_return_to] ||
39+
dashboard_path
3940
end
4041
end

config/feature_flags.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ pds_lookup_during_import: Perform PDS lookups as part of the patient import
2121
processing.
2222

2323
testing_api: Basic API useful for automated testing.
24+
25+
reporting_api: Enables the Commissioner reporting component to authenticate to Mavis via OAUTH 2.0
26+
Authorization Code Flow (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1), and retrieve
27+
statistics from /api/reporting/

config/routes.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@
108108
resources :teams, only: :destroy, param: :workgroup
109109
post "/onboard", to: "onboard#create"
110110
end
111+
namespace :reporting do
112+
post "authorize", to: "one_time_tokens#authorize"
113+
get "totals", controller: :totals, action: :index
114+
end
111115
end
112116
end
113117

config/settings.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ splunk:
4949
reporting_api:
5050
client_app:
5151
token_ttl_seconds: 600
52+
root_url:
53+
secret:

config/settings/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ splunk:
2424
reporting_api:
2525
client_app:
2626
root_url: http://localhost:5001/
27+
secret: ""
28+
client_id: "testing_client_id"
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe API::Reporting::OneTimeTokensController do
6+
let(:user) { create(:user) }
7+
let(:mock_cis2_info) { { "some_key" => "some value" } }
8+
let(:valid_token) do
9+
ReportingAPI::OneTimeToken.find_or_generate_for!(
10+
user:,
11+
cis2_info: mock_cis2_info
12+
)
13+
end
14+
let(:invalid_token) { SecureRandom.hex(32) }
15+
16+
describe "#authorize" do
17+
context "given a valid client_id when reporting_api is enabled" do
18+
before { Flipper.enable(:reporting_api) }
19+
20+
let(:client_id) { Settings.reporting_api.client_app.client_id }
21+
let(:grant_type) { "some_grant_type" }
22+
23+
let(:do_the_request) do
24+
post :authorize,
25+
params: {
26+
code: token.token,
27+
grant_type: grant_type,
28+
client_id: client_id
29+
},
30+
format: :json
31+
end
32+
33+
context "and a valid OneTimeToken in the code param" do
34+
let(:token) { valid_token }
35+
36+
context "and a grant_type which is not authorization_code" do
37+
let(:grant_type) { "not_an_authorization_code" }
38+
39+
it "responds with HTTP 400" do
40+
do_the_request
41+
expect(response.status).to eq(400)
42+
end
43+
44+
describe "the response json" do
45+
let(:response_json) { JSON.parse(response.body) }
46+
47+
it "has an error key set to unsupported_grant_type" do
48+
do_the_request
49+
expect(response_json["error"]).to eq("unsupported_grant_type")
50+
end
51+
end
52+
end
53+
end
54+
55+
context "and a grant_type of authorization_code" do
56+
# this param name and value is required by the OAUTH 2.0 spec
57+
# see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
58+
let(:grant_type) { "authorization_code" }
59+
60+
context "and a valid OneTimeToken in the code param" do
61+
let(:token) { valid_token }
62+
63+
it "responds with 200" do
64+
do_the_request
65+
expect(response.status).to eq(200)
66+
end
67+
68+
it "deletes the OneTimeToken" do
69+
do_the_request
70+
expect(ReportingAPI::OneTimeToken.exists?(token.id)).to be(false)
71+
end
72+
73+
it "responds with json" do
74+
do_the_request
75+
expect(response.headers["Content-type"]).to eq(
76+
"application/json; charset=utf-8"
77+
)
78+
end
79+
80+
describe "the response json" do
81+
let(:response_json) { JSON.parse(response.body) }
82+
83+
it "includes a JWT" do
84+
do_the_request
85+
expect(response_json["jwt"]).not_to be_empty
86+
end
87+
88+
describe "the JWT payload" do
89+
let(:payload) { response_json["jwt"] }
90+
91+
it "is encoded with the Mavis reporting app secret" do
92+
do_the_request
93+
expect {
94+
JWT.decode(
95+
response_json["jwt"],
96+
Settings.reporting_api.client_app.secret,
97+
true,
98+
{ algorithm: "HS512" }
99+
)
100+
}.not_to raise_error
101+
end
102+
103+
describe "once decoded" do
104+
let(:decoded_payload) do
105+
JWT.decode(
106+
response_json["jwt"],
107+
Settings.reporting_api.client_app.secret,
108+
true,
109+
{ algorithm: "HS512" }
110+
)
111+
end
112+
let(:jwt_data) { decoded_payload.first["data"] }
113+
114+
it "includes the user attributes" do
115+
do_the_request
116+
expect(jwt_data["user"]).to eq(user.as_json)
117+
end
118+
119+
it "includes the users cis2_info" do
120+
do_the_request
121+
expect(jwt_data["cis2_info"]).to eq(mock_cis2_info)
122+
end
123+
end
124+
end
125+
end
126+
end
127+
128+
context "and a OneTimeToken in the code param which can't be found in the users table" do
129+
let(:do_the_request) do
130+
post :authorize,
131+
params: {
132+
code: invalid_token,
133+
grant_type: grant_type,
134+
client_id: client_id
135+
},
136+
format: :json
137+
end
138+
let(:response_json) { JSON.parse(response.body) }
139+
140+
it "returns 403" do
141+
do_the_request
142+
expect(response.status).to eq(403)
143+
end
144+
145+
it "returns an error in the body" do
146+
do_the_request
147+
expect(response_json).to eq({ "errors" => "invalid_grant" })
148+
end
149+
end
150+
end
151+
end
152+
end
153+
end

0 commit comments

Comments
 (0)