Skip to content

Commit cb6c725

Browse files
authored
Merge pull request #3680 from mlibrary/HELIO-4561/editor_ebook_share_link_draft_sibling_access
HELIO-4561 - share link access to draft Monograph
2 parents 96ac396 + 025fcf6 commit cb6c725

16 files changed

+575
-96
lines changed

app/controllers/e_pubs_controller.rb

+7-7
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,16 @@ def download_interval # rubocop:disable Metrics/CyclomaticComplexity, Metrics/Pe
149149
def share_link
150150
return head :no_content unless @policy.show?
151151

152-
subdomain = @presenter.parent.subdomain
152+
subdomain = @parent_presenter.subdomain
153153
if Press.where(subdomain: subdomain).first&.allow_share_links?
154154
expire = Time.now.to_i + 28 * 24 * 3600 # 28 days in seconds
155-
token = JsonWebToken.encode(data: @noid, exp: expire)
155+
token = JsonWebToken.encode(data: @parent_presenter.id, exp: expire)
156156
ShareLinkLog.create(ip_address: request.ip,
157157
institution: current_institutions.map(&:name).join("|"),
158158
press: subdomain,
159159
user: current_actor.email,
160-
title: @presenter.parent.title,
161-
noid: @presenter.id,
160+
title: @parent_presenter.title,
161+
noid: @parent_presenter.id,
162162
token: token,
163163
action: 'create')
164164
render plain: Rails.application.routes.url_helpers.epub_url(@noid, share: token)
@@ -191,7 +191,7 @@ def log_share_link_use
191191
press: @subdomain,
192192
user: current_actor.email,
193193
title: @parent_presenter.title,
194-
noid: @noid,
194+
noid: @parent_presenter.id,
195195
token: @share_link,
196196
action: 'use')
197197
end
@@ -200,8 +200,8 @@ def valid_share_link?
200200
if @share_link.present?
201201
begin
202202
decoded = JsonWebToken.decode(@share_link)
203-
return true if decoded[:data] == @noid
204-
rescue JWT::ExpiredSignature
203+
return true if decoded[:data] == @noid || decoded[:data] == @parent_presenter.id
204+
rescue JWT::ExpiredSignature, JWT::VerificationError
205205
return false
206206
end
207207
end

app/controllers/monograph_catalog_controller.rb

+26-2
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,37 @@ def item_identifier_for_irus_analytics
108108

109109
private
110110

111+
def valid_share_link?
112+
return @valid_share_link if @valid_share_link.present?
113+
114+
share_link = params[:share] || session[:share_link]
115+
session[:share_link] = share_link
116+
117+
@valid_share_link = if share_link.present?
118+
begin
119+
decoded = JsonWebToken.decode(share_link)
120+
true if decoded[:data] == @monograph_presenter&.id
121+
rescue JWT::ExpiredSignature, JWT::VerificationError
122+
false
123+
end
124+
else
125+
false
126+
end
127+
end
128+
111129
instrument_method
112130
def load_presenter
113131
retries ||= 0
114132
monograph_id = params[:monograph_id] || params[:id]
115133
@monograph_presenter = Hyrax::PresenterFactory.build_for(ids: [monograph_id], presenter_class: Hyrax::MonographPresenter, presenter_args: current_ability).first
116134
raise PageNotFoundError if @monograph_presenter.nil?
117-
raise CanCan::AccessDenied unless current_ability&.can?(:read, @monograph_presenter)
135+
136+
# We don't "sell"/protect the Monograph catalog page. This line is the one and only place where the Monograph's...
137+
# draft (restricted) status can prevent it being universally seen.
138+
# Share links being used to "expose" the Monograph page are only for editors allowing, e.g. authors to easily...
139+
# review draft content. We can assume the Monograph is draft if the first condition is false.
140+
raise CanCan::AccessDenied unless current_ability&.can?(:read, @monograph_presenter) || valid_share_link?
141+
118142
rescue RSolr::Error::ConnectionRefused, RSolr::Error::Http => e
119143
Rails.logger.error(%Q|[RSOLR ERROR TRY:#{retries}] #{e} #{e.backtrace.join("\n")}|)
120144
retries += 1
@@ -128,7 +152,7 @@ def monograph_auth_for
128152
@ebook_download_presenter = EBookDownloadPresenter.new(@monograph_presenter, current_ability, current_actor)
129153
# The monograph catalog page is completely user-facing, apart from a small admin menu. The "Read" button should...
130154
# never show up if there is no published ebook for CSB to use! This is important for the "Forthcoming" workflow.
131-
@show_read_button = @monograph_presenter.reader_ebook? && @monograph_presenter&.reader_ebook['visibility_ssi'] == 'open'
155+
@show_read_button = @monograph_presenter.reader_ebook? && (@monograph_presenter&.reader_ebook['visibility_ssi'] == 'open' || valid_share_link?)
132156
@disable_read_button = disable_read_button?
133157
end
134158

app/models/monograph_search_builder.rb

+21
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ class MonographSearchBuilder < ::SearchBuilder
1010
:filter_out_tombstones
1111
]
1212

13+
# this prevents the Solr request from filtering draft documents out, given an anonymous user using a share link,...
14+
# otherwise the following would be added to `fq`, removing such documents from the response completely:
15+
# ({!terms f=edit_access_group_ssim}public) OR ({!terms f=discover_access_group_ssim}public) OR ({!terms f=read_access_group_ssim}public)"
16+
self.default_processor_chain -= [:add_access_controls_to_solr_params] if :valid_share_link?
17+
1318
instrument_method
1419
def filter_by_monograph_id(solr_parameters)
1520
id = monograph_id(blacklight_params)
@@ -48,6 +53,22 @@ def filter_out_tombstones(solr_parameters)
4853
solr_parameters[:fq] << "-tombstone_ssim:[* TO *]"
4954
end
5055

56+
def valid_share_link?
57+
share_link = blacklight_params[:share] || session[:share_link]
58+
session[:share_link] = share_link
59+
60+
if share_link.present?
61+
begin
62+
decoded = JsonWebToken.decode(share_link)
63+
return true if decoded[:data] == @monograph_presenter&.id
64+
rescue JWT::ExpiredSignature
65+
false
66+
end
67+
else
68+
false
69+
end
70+
end
71+
5172
private
5273

5374
def monograph_id(blacklight_params)

app/overrides/hyrax/downloads_controller_overrides.rb

+7-7
Original file line numberDiff line numberDiff line change
@@ -104,29 +104,29 @@ def allow_download?
104104

105105
# HELIO-4501 Override to use the Hyrax 3.4 version of this with no workflow related code.
106106
def authorize_download!
107-
return true if authorize_embeds_for_epub_share_link?
107+
return true if authorize_thumbs_and_embeds_for_share_link?
108108

109109
authorize! :download, params[asset_param_key]
110110
rescue CanCan::AccessDenied
111111
unauthorized_image = Rails.root.join("app", "assets", "images", "unauthorized.png")
112112
send_file unauthorized_image, status: :unauthorized
113113
end
114114

115-
def authorize_embeds_for_epub_share_link?
115+
def authorize_thumbs_and_embeds_for_share_link?
116116
# adding some logic to allow *draft* FileSet "downloads" to work when a session holds the sibling EPUB's share link.
117117
# This is specifically so that draft embedded video, jpeg (video poster), audio and animated gif resources will display in CSB.
118118
# Images will work anyway seeing as RIIIF tiles get served regardless of the originating FileSet's publication status.
119119

120-
if presenter.visibility == 'restricted' && presenter&.parent&.epub? && (jpeg? || video? || sound? || animated_gif? || closed_captions? || visual_descriptions?)
121-
# I think the link could only be in the session here, but will check for `params[:share]` anyway
122-
share_link = params[:share] || session[:share_link]
123-
session[:share_link] = share_link
120+
share_link = params[:share] || session[:share_link]
121+
session[:share_link] = share_link if share_link.present?
124122

123+
# note we're *not* authorizing *all* FileSet downloads from Fedora here, just those tied to display
124+
if thumbnail? || jpeg? || video? || sound? || animated_gif? || closed_captions? || visual_descriptions?
125125
if share_link.present?
126126
begin
127127
decoded = JsonWebToken.decode(share_link)
128128

129-
return true if decoded[:data] == presenter&.parent&.epub_id
129+
return true if decoded[:data] == presenter&.parent&.id
130130
rescue JWT::ExpiredSignature
131131
false
132132
end

app/overrides/hyrax/file_sets_controller_overrides.rb

+23-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,29 @@ def reindex
135135
# See hyrax, https://github.com/samvera/hyrax/commit/cb21570fadcea0b8d1dfd0b7cffecf5135c1ea76
136136
# See hyrax, https://github.com/samvera/hyrax/commit/1efe93929285985751cc270675c243f628cf31ca
137137
def presenter
138-
@presenter ||= show_presenter.new(curation_concern_document, current_ability, request)
138+
@presenter ||= if valid_share_link?
139+
@share_link_presenter
140+
else
141+
show_presenter.new(curation_concern_document, current_ability, request)
142+
end
143+
end
144+
145+
def valid_share_link?
146+
return @valid_share_link if @valid_share_link.present?
147+
148+
share_link = params[:share] || session[:share_link]
149+
session[:share_link] = share_link
150+
151+
@valid_share_link = if share_link.present?
152+
@share_link_presenter = show_presenter.new(::SolrDocument.find(params[:id]), current_ability, request)
153+
154+
begin
155+
decoded = JsonWebToken.decode(share_link)
156+
true if decoded[:data] == @share_link_presenter&.parent&.id
157+
rescue JWT::ExpiredSignature
158+
false
159+
end
160+
end
139161
end
140162
end)
141163
end

app/presenters/concerns/common_work_presenter.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@ def non_representative_file_set_ids # rubocop:disable Metrics/CyclomaticComplexi
2222
@non_representative_file_set_ids = file_sets_ids
2323
end
2424

25-
def assets?
26-
ordered_file_sets_ids.present?
25+
def assets?(valid_share_link = false)
26+
ordered_file_sets_ids(valid_share_link).present?
2727
end
2828

29-
def ordered_file_sets_ids # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
29+
def ordered_file_sets_ids(valid_share_link = false) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
3030
return @ordered_file_sets_ids if @ordered_file_sets_ids
3131
file_sets_ids = []
3232
ordered_member_docs.each do |doc|
3333
next if doc['has_model_ssim'] != ['FileSet'].freeze
3434
next if doc.id == representative_id
3535
next if featured_representatives.map(&:file_set_id).include?(doc.id)
36-
next if doc['visibility_ssi'] != 'open' && !current_ability&.can?(:read, doc.id)
36+
next if doc['visibility_ssi'] != 'open' && !(current_ability&.can?(:read, doc.id) || valid_share_link)
3737

3838
file_sets_ids.append doc.id
3939
end

app/presenters/concerns/embed_code_presenter.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def embeddable_type?
88
end
99

1010
def allow_embed?
11-
current_ability.platform_admin?
11+
current_ability&.platform_admin?
1212
end
1313

1414
def embed_code

app/views/monograph_catalog/_index_monograph.html.erb

+1-1
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@
323323
</div><!-- /.monograph-info-epub -->
324324

325325
<!-- RESOURCES -->
326-
<% if @monograph_presenter.assets? %>
326+
<% if @monograph_presenter.assets?(@valid_share_link) %>
327327
<div class="row monograph-assets">
328328
<div class="col-sm-12">
329329
<h2>Resources</h2>

spec/controllers/e_pubs_controller_spec.rb

+6-6
Original file line numberDiff line numberDiff line change
@@ -677,13 +677,13 @@
677677

678678
context "A restricted epub" do
679679
let(:valid_share_token) do
680-
JsonWebToken.encode(data: file_set.id, exp: Time.now.to_i + 48 * 3600)
680+
JsonWebToken.encode(data: monograph.id, exp: Time.now.to_i + 28 * 24 * 3600)
681681
end
682682
let(:expired_share_token) do
683-
JsonWebToken.encode(data: file_set.id, exp: Time.now.to_i - 1000)
683+
JsonWebToken.encode(data: monograph.id, exp: Time.now.to_i - 1000)
684684
end
685685
let(:wrong_share_token) do
686-
JsonWebToken.encode(data: 'wrongnoid', exp: Time.now.to_i + 48 * 3600)
686+
JsonWebToken.encode(data: 'wrongnoid', exp: Time.now.to_i + 28 * 24 * 3600)
687687
end
688688
let(:parent) { Sighrax.from_noid(monograph.id) }
689689
let(:epub) { Sighrax.from_noid(file_set.id) }
@@ -701,7 +701,7 @@
701701
expect(ShareLinkLog.last.action).to eq 'use'
702702
expect(ShareLinkLog.last.user).to be_nil
703703
expect(ShareLinkLog.last.title).to eq monograph.title.first
704-
expect(ShareLinkLog.last.noid).to eq file_set.id
704+
expect(ShareLinkLog.last.noid).to eq monograph.id
705705
expect(ShareLinkLog.last.token).to eq valid_share_token
706706
end
707707

@@ -735,7 +735,7 @@
735735
expect(ShareLinkLog.last.action).to eq 'use'
736736
expect(ShareLinkLog.last.user).to eq user.email
737737
expect(ShareLinkLog.last.title).to eq monograph.title.first
738-
expect(ShareLinkLog.last.noid).to eq file_set.id
738+
expect(ShareLinkLog.last.noid).to eq monograph.id
739739
expect(ShareLinkLog.last.token).to eq valid_share_token
740740
end
741741

@@ -793,7 +793,7 @@
793793
it 'returns a share link with a valid JSON webtoken and logs the creation' do
794794
get :share_link, params: { id: '222222222' }
795795
expect(response).to have_http_status(:success)
796-
expect(response.body).to eq "http://test.host/epubs/222222222?share=#{JsonWebToken.encode(data: '222222222', exp: now.to_i + share_link_expiration_time)}"
796+
expect(response.body).to eq "http://test.host/epubs/222222222?share=#{JsonWebToken.encode(data: '111111111', exp: now.to_i + share_link_expiration_time)}"
797797
expect(ShareLinkLog.count).to eq 1
798798
expect(ShareLinkLog.last.action).to eq 'create'
799799
end

spec/controllers/hyrax/downloads_controller_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@
431431
}
432432

433433
let(:valid_share_token) do
434-
JsonWebToken.encode(data: draft_epub_file_set.id, exp: Time.now.to_i + 48 * 3600)
434+
JsonWebToken.encode(data: monograph.id, exp: Time.now.to_i + 28 * 24 * 3600)
435435
end
436436

437437
before do

spec/controllers/hyrax/file_sets_controller_spec.rb

+22
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@
2828
expect(response).to redirect_to(Hyrax::Engine.routes.url_helpers.download_path(file_set.id))
2929
expect(response).to have_http_status(:found) # 302 Found
3030
end
31+
32+
context 'draft FileSet with an anonymous user' do
33+
before { sign_out user }
34+
35+
it 'Redirects to login' do
36+
get :show, params: { id: file_set.id }
37+
expect(response).to redirect_to('/login?locale=en')
38+
expect(response).to have_http_status(:found) # 302 Found
39+
end
40+
41+
context 'Using a share link for the parent Monograph' do
42+
let(:valid_share_token) do
43+
JsonWebToken.encode(data: monograph.id, exp: Time.now.to_i + 28 * 24 * 3600)
44+
end
45+
46+
it 'succeeds' do
47+
get :show, params: { id: file_set.id, share: valid_share_token }
48+
expect(response).to have_http_status(:success)
49+
expect(response).to render_template('show')
50+
end
51+
end
52+
end
3153
end
3254

3355
describe "#destroy" do

spec/controllers/monograph_catalog_controller_spec.rb

+26
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@
104104
get :index, params: { id: monograph.id }
105105
expect(assigns(:show_read_button)).to eq false
106106
end
107+
108+
context 'with a valid share link' do
109+
let(:valid_share_token) do
110+
JsonWebToken.encode(data: monograph.id, exp: Time.now.to_i + 28 * 24 * 3600)
111+
end
112+
113+
it "shows the read button" do
114+
get :index, params: { id: monograph.id, share: valid_share_token }
115+
expect(assigns(:show_read_button)).to eq true
116+
end
117+
end
107118
end
108119

109120
context 'public ebook FeaturedRepresentative FileSet' do
@@ -204,6 +215,21 @@
204215
it 'redirects to login page' do
205216
expect(response).to redirect_to(new_user_session_path)
206217
end
218+
219+
context 'with a valid share link' do
220+
let(:valid_share_token) do
221+
JsonWebToken.encode(data: monograph.id, exp: Time.now.to_i + 28 * 24 * 3600)
222+
end
223+
224+
before do
225+
get :index, params: { id: monograph.id, share: valid_share_token }
226+
end
227+
228+
it 'response is successful' do
229+
expect(response).to be_successful
230+
expect(response).to render_template('monograph_catalog/index')
231+
end
232+
end
207233
end
208234

209235
context 'logged-in read user (depositor)' do

0 commit comments

Comments
 (0)