diff --git a/app/controllers/api/zotero_controller.rb b/app/controllers/api/zotero_controller.rb new file mode 100644 index 000000000..575ca14fb --- /dev/null +++ b/app/controllers/api/zotero_controller.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true +# This entire file is being overriden for the change mentioned in the TODO below +require 'oauth' + +module API + # Adds the ability to authenticate against Zotero's OAuth endpoint + class ZoteroController < ApplicationController + before_action :authenticate_user! + before_action :authorize_user! + before_action :validate_params, only: :callback + + def initiate + request_token = client.get_request_token(oauth_callback: callback_url) + session[:request_token] = request_token + current_user.zotero_token = request_token + current_user.save + redirect_to request_token.authorize_url(identity: '1', oauth_callback: callback_url) + rescue OAuth::Unauthorized + redirect_to root_url, alert: 'Invalid Zotero client key pair' + end + + def callback + access_token = current_token.get_access_token(oauth_verifier: params['oauth_verifier']) + # parse userID and API key out of token and store in user instance + current_user.zotero_userid = access_token.params[:userID] + current_user.save + + # TODO: we are overriding this entire file to a .user_key on to the end of current_user + # This file should be removed once sufia or hyrax have this update and we are on that version + Sufia::Arkivo::CreateSubscriptionJob.perform_later(current_user.user_key) + redirect_to sufia.profile_path(current_user), notice: 'Successfully connected to Zotero!' + rescue OAuth::Unauthorized + redirect_to sufia.edit_profile_path(current_user.to_param), alert: 'Please re-authenticate with Zotero' + ensure + current_user.zotero_token = nil + current_user.save + end + + private + + def authorize_user! + authorize! :create, Sufia.primary_work_type + rescue CanCan::AccessDenied + return redirect_to root_url, alert: 'You are not authorized to perform this operation' + end + + def validate_params + return redirect_to sufia.edit_profile_path(current_user.to_param), alert: "Malformed request from Zotero" if params[:oauth_token].blank? || params[:oauth_verifier].blank? + return redirect_to sufia.edit_profile_path(current_user.to_param), alert: "You have not yet connected to Zotero" if !current_token || current_token.params[:oauth_token] != params[:oauth_token] + end + + def client + ::OAuth::Consumer.new(Sufia::Zotero.config['client_key'], Sufia::Zotero.config['client_secret'], options) + end + + def current_token + current_user.zotero_token + end + + def callback_url + "#{request.base_url}/api/zotero/callback" + end + + def options + { + site: 'https://www.zotero.org', + scheme: :query_string, + http_method: :get, + request_token_path: '/oauth/request', + access_token_path: '/oauth/access', + authorize_path: '/oauth/authorize' + } + end + end +end diff --git a/spec/controllers/zotero_controller_spec.rb b/spec/controllers/zotero_controller_spec.rb new file mode 100644 index 000000000..bdd92ee2c --- /dev/null +++ b/spec/controllers/zotero_controller_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true +require 'rails_helper' + +describe API::ZoteroController, type: :controller do + let(:user) { create(:user) } + routes { Sufia::Engine.routes } + + subject { response } + + context 'with an HTTP GET to /api/zotero' do + context 'with an unauthenticated client' do + before { get :initiate } + + specify do + expect(subject).to have_http_status(302) + expect(subject).to redirect_to("https://webaccess.psu.edu/?cosign-localhost&https://localhost") + end + end + + context 'with an unregistered user' do + before do + allow_any_instance_of(Ability).to receive(:can?).with(:create, GenericWork).and_return(false) + sign_in user + get :initiate + end + + specify do + expect(subject).to have_http_status(302) + expect(subject).to redirect_to(root_path) + expect(flash[:alert]).to eq 'You are not authorized to perform this operation' + end + end + + context 'with an invalid key/secret combo' do + before do + allow(Sufia::Zotero).to receive(:config) { broken_config } + sign_in user + get :initiate + end + + let(:broken_config) { Hash.new(client_key: 'foo', client_secret: 'bar') } + + specify do + expect(subject).to have_http_status(302) + expect(subject).to redirect_to(root_path) + expect(flash[:alert]).to eq 'Invalid Zotero client key pair' + end + end + + describe 'redirects to Zotero' do + before do + allow(controller).to receive(:client) { client } + allow(client).to receive(:get_request_token) { token } + allow_any_instance_of(User).to receive(:zotero_token=) + sign_in user + get :initiate + end + + let(:token) { object_double(OAuth::RequestToken.new(client), authorize_url: 'https://www.zotero.org/oauth/authorize?identity=1&oauth_callback=http%3A%2F%2Ftest.host%2Fapi%2Fzotero%2Fcallback&oauth_token=bc2502f2750983c57224') } + let(:client) do + OAuth::Consumer.new(Sufia::Zotero.config['client_key'], + Sufia::Zotero.config['client_secret'], + site: 'https://www.zotero.org', + scheme: :query_string, + http_method: :get, + request_token_path: '/oauth/request', + access_token_path: '/oauth/access', + authorize_path: '/oauth/authorize') + end + + specify do + expect(subject).to have_http_status(302) + expect(flash[:alert]).to be_nil + expect(subject.headers['Location']).to include('oauth_callback=http%3A%2F%2Ftest.host%2Fapi%2Fzotero%2Fcallback') + end + end + end + + context 'with an HTTP POST/GET to /api/zotero/callback' do + context 'with an unauthenticated user' do + before { get :callback } + + specify do + expect(subject).to have_http_status(302) + expect(subject).to redirect_to("https://webaccess.psu.edu/?cosign-localhost&https://localhost") + end + end + + context 'with a user who is not permitted to make works' do + before do + allow_any_instance_of(Ability).to receive(:can?).with(:create, GenericWork).and_return(false) + sign_in user + get :callback + end + + specify do + expect(subject).to have_http_status(302) + expect(subject).to redirect_to(root_path) + expect(flash[:alert]).to eq 'You are not authorized to perform this operation' + end + end + + context 'with a request lacking an oauth_token' do + before do + sign_in user + get :callback + end + + specify do + expect(subject).to have_http_status(302) + expect(subject).to redirect_to(routes.url_helpers.edit_profile_path(user)) + expect(flash[:alert]).to eq 'Malformed request from Zotero' + end + end + + context 'with a non-matching token' do + before do + sign_in user + get :callback, oauth_token: 'woohoo', oauth_verifier: '12345' + end + + specify do + expect(subject).to have_http_status(302) + expect(subject).to redirect_to(routes.url_helpers.edit_profile_path(user)) + expect(flash[:alert]).to eq 'You have not yet connected to Zotero' + end + end + + context 'with a signed-in, valid user' do + before do + allow_any_instance_of(User).to receive(:zotero_token) { user_token } + allow(Sufia::Arkivo::CreateSubscriptionJob).to receive(:perform_later) + sign_in user + get :callback, oauth_token: token_string, oauth_verifier: pin + end + + let(:token_string) { 'woohoo' } + let(:pin) { '12345' } + let(:user_token) do + double('token', + params: { oauth_token: token_string }, + get_access_token: access_token) + end + let(:zuserid) { 'myzuser' } + let(:access_token) do + double('access', params: { userID: zuserid }) + end + + specify do + expect(subject).to have_http_status(302) + expect(Sufia::Arkivo::CreateSubscriptionJob).to have_received(:perform_later).with(user.user_key) + expect(subject).to redirect_to(routes.url_helpers.profile_path(user)) + expect(flash[:alert]).to be_nil + expect(flash[:notice]).to eq 'Successfully connected to Zotero!' + expect(user.reload.zotero_userid).to eq zuserid + end + end + end +end