Skip to content

Commit

Permalink
Merge pull request #3685 from mlibrary/HELIO-4726/counter-redirect-to…
Browse files Browse the repository at this point in the history
…-scholarlyiq

HELIO-4726 Forward users from /counter_reports to scholarlyiq
  • Loading branch information
conorom authored Sep 16, 2024
2 parents c407ea4 + 6a4d20c commit 0b92786
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 9 deletions.
11 changes: 6 additions & 5 deletions app/controllers/counter_reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ def platforms
def index
return render if @counter_report_service.present?

render 'counter_reports/without_customer_id/index'
config = Rails.root.join('config', 'scholarlyiq.yml')
if Flipflop.scholarlyiq_counter_redirect? && File.exist?(config)
redirect_to ScholarlyiqRedirectUrlService.encrypted_url(config, @institutions)
else
render 'counter_reports/without_customer_id/index'
end
end

def edit
Expand Down Expand Up @@ -110,12 +115,8 @@ def set_counter_report
def set_presses_and_institutions
return if @counter_report_service.present?

# HELIO-3526 press admins (and editors, analysts or other authed and prived users) will no longer
# access reports through this controller, but instead through the dashboard and
# hyrax/admin/stats_controller.rb
@institutions = current_institutions.sort_by(&:name)
@presses = Press.order(:name)

if @institutions.empty? || @presses.empty?
@skip_footer = true
render 'counter_reports/unauthorized', status: :unauthorized
Expand Down
33 changes: 33 additions & 0 deletions app/services/scholarlyiq_redirect_url_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

class ScholarlyiqRedirectUrlService
def self.encrypted_url(config, institutions)
yaml = YAML.safe_load(File.read(config))
url = yaml["siq_portal_url"]
secret = yaml["siq_shared_secret"] # AES-128-GCM requires a 16-byte key
iv = SecureRandom.random_bytes(12) # AES-128-GCM require a 12 byte nonce

cipher = OpenSSL::Cipher.new("aes-128-gcm")
cipher.encrypt
cipher.key = secret
cipher.iv = iv

payload = {}
payload["siteIds"] = []
institutions.each do |inst|
payload["siteIds"] << inst.identifier
end

encrypted_payload = cipher.update(payload.to_json) + cipher.final
auth_tag = cipher.auth_tag

encoded_encrypted_payload = Base64.strict_encode64(encrypted_payload + auth_tag)
encoded_nonce = Base64.strict_encode64(iv)

url_encoded_encrypted_payload = CGI.escape(encoded_encrypted_payload)
url_encoded_nonce = CGI.escape(encoded_nonce)

token = "#{url_encoded_nonce}:#{url_encoded_encrypted_payload}"
url + "?token=#{token}"
end
end
9 changes: 9 additions & 0 deletions config/features.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

# Add fulcrum specific flipflop features here

Flipflop.configure do
feature :scholarlyiq_counter_redirect,
default: false,
description: "Redirect known users from /counter_report to scholarlyiq"
end
4 changes: 4 additions & 0 deletions config/scholarlyiq.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ Bucket: "s3 bucket"
BucketRegion: "s3 bucket region"
AwsAccessKeyId: 123456789XYZ
AwsSecretAccessKey: AwsSecretAccessKeyAwsSecretAccessKey

# HELIO-4726 for redirecting users to SIQ to see their reports
siq_portal_url: 'scholarlyiq.com'
siq_shared_secret: 'some-shared-secret'
29 changes: 25 additions & 4 deletions spec/requests/counter_reports_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,31 @@
end

describe "GET /counter_reports" do
it do
get counter_reports_path
expect(response).to have_http_status(:ok)
expect(response).to render_template(:index)
context "with scholarlyiq redirect" do
before do
allow(Flipflop).to receive(:scholarlyiq_counter_redirect?).and_return(true)
allow(File).to receive(:exist?).and_return(true)
allow(File).to receive(:read).and_return(true)
allow(YAML).to receive(:safe_load).and_return(yaml)
end

let(:yaml) { { 'siq_portal_url' => 'http://test.scholarlyiq.com', 'siq_shared_secret' => '0123456789123456' } }
it do
get counter_reports_path
expect(response).to have_http_status(:found)
end
end

context "without scholarlyiq redirect" do
before do
allow(Flipflop).to receive(:scholarlyiq_counter_redirect?).and_return(false)
end

it do
get counter_reports_path
expect(response).to have_http_status(:ok)
expect(response).to render_template(:index)
end
end
end

Expand Down
57 changes: 57 additions & 0 deletions spec/services/scholarlyiq_redirect_url_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ScholarlyiqRedirectUrlService do
describe "#encrypted_url" do
let(:institutions) do
[
create(:institution, identifier: "1"),
create(:institution, identifier: "10")
]
end

let(:shared_secret) { "0123456789123456" } # 16 byte shared secret
let(:nonce) { "012345678912" } # 12 byte nonce
let(:yaml) { { "siq_portal_url" => "http://test.scholarlyiq.com", "siq_shared_secret" => shared_secret } }
let(:config) { double("config") }

before do
allow(File).to receive(:read).and_return(true)
allow(YAML).to receive(:safe_load).and_return(yaml)
allow(SecureRandom).to receive(:random_bytes).with(12).and_return(nonce)
end

it "correctly builds the encrypted url" do
# Just a check that the payload we encrpyted decrypts correctly
redirect_url = described_class.encrypted_url(config, institutions)

# Extract the token from the URL
token_param = redirect_url.match(/token=([^&]+)/)[1]

# Split the token into nonce and encrypted payload parts
url_encoded_nonce, url_encoded_encrypted_payload = token_param.split(':')

# URL-decode and Base64 decode the nonce and the encrypted payload
iv = Base64.decode64(CGI.unescape(url_encoded_nonce))
encrypted_payload_with_auth_tag = Base64.decode64(CGI.unescape(url_encoded_encrypted_payload))

# Extract the ciphertext and authentication tag
auth_tag = encrypted_payload_with_auth_tag[-16..-1]
encrypted_payload = encrypted_payload_with_auth_tag[0..-17]

# Create a Cipher for AES-128-GCM decryption
cipher = OpenSSL::Cipher.new('aes-128-gcm')
cipher.decrypt
cipher.key = shared_secret
cipher.iv = iv # use the decoded 12-byte nonce
cipher.auth_tag = auth_tag

# Decrypt the payload
decrypted_payload = cipher.update(encrypted_payload) + cipher.final

expect(iv).to eq nonce
expect(JSON.parse(decrypted_payload)).to eq({ "siteIds" => ["1", "10"] })
end
end
end

0 comments on commit 0b92786

Please sign in to comment.