Create new InstFS references for RCE links

closes RCX-1900
flag=rce_linked_file_urls

Test plan
- Have InstFS running and enabled with Canvas
- Run API calls to api/v1/rce_linked_file_urls
- Send an arbitrary "location" parameter
- Send a user_uuid parameter (with access, without, see
  what happens)
- Send an array of file_urls with various types of
  Canvas files that aren't previewable with quizzes
  (should be media and documents that aren't)
- Verify that you get a new InstFS uuid for the files
- Extra credit if you check that the location is saved
  properly on the new file in InstFS's DynamoDB

Change-Id: I4d7eb1aeb62a515360fc4668acbe38caa8f94ed8
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/354570
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: James Logan <james.logan@instructure.com>
Reviewed-by: Eric Saupe <eric.saupe@instructure.com>
QA-Review: James Logan <james.logan@instructure.com>
Product-Review: Mysti Lilla <mysti@instructure.com>
This commit is contained in:
Mysti Lilla 2024-08-07 20:29:40 -06:00
parent 523469c087
commit bcc2dd549b
4 changed files with 104 additions and 36 deletions

View File

@ -1572,7 +1572,7 @@ class FilesController < ApplicationController
nil
end
att_ids = parsed_file_urls.pluck(:id, :attachment_id, :file_id).flatten.compact
att_list = Attachment.where(id: att_ids).merge(Attachment.active.or(Attachment.where.not(replacement_attachment_id: nil))).preload(:context, replacement_attachment: :context)
att_list = Attachment.where(id: att_ids).merge(Attachment.not_deleted.or(Attachment.where.not(replacement_attachment_id: nil))).preload(:context, replacement_attachment: :context)
att_hash_list = att_list.index_by { |att| att.id.to_s }
file_context_keys = %i[account_id course_id group_id user_id].freeze
@ -1581,7 +1581,7 @@ class FilesController < ApplicationController
file_id = parsed_file_url[:id] || parsed_file_url[:attachment_id] || parsed_file_url[:file_id]
att = att_hash_list[file_id]
if att&.replacement_attachment_id
parsed_file_url[:old_att_id] = att.id
parsed_file_url[:id] ||= att.id
att = att.replacement_attachment
end
next unless att
@ -1599,18 +1599,32 @@ class FilesController < ApplicationController
next unless context.grants_any_right?(@current_user, session, :manage_files_create, :manage_files_edit, :moderate_user_content, :become_user) &&
context.grants_any_right?(user, session, :manage_files_create, :manage_files_edit)
canvas_display_files = []
file_list.each do |file|
att = file[:attachment]
if att.media_entry_id.present? || att.canvadocable?
file_metadata[:canvas_urls] ||= {}
url = file[:url]
old_att_id = file[:old_att_id]
file_metadata[:canvas_urls][file[:url]] = old_att_id.present? ? url.sub(%r{(files|iframe)/#{old_att_id}}, "\\1/#{att.id}") : url
canvas_display_files << file
else
file_metadata[:instfs_ids] ||= {}
file_metadata[:instfs_ids][file[:url]] = att.instfs_uuid
end
end
canvas_display_files.each do |file|
file_metadata[:canvas_urls] ||= {}
# TODO: Work with InstFS to make a bulk duplicate API for this
url = file[:url]
att = file[:attachment]
unless params[:location].present? && (new_instfs_ref = att.create_rce_reference(params[:location]))
file_metadata[:canvas_urls][url] = url
next
end
old_att_id = file[:id] if file[:id] != att.id.to_s
new_url = old_att_id.present? ? url.sub(%r{(files|iframe)/#{old_att_id}}, "\\1/#{att.id}") : url
parsed_url = Addressable::URI.parse(CGI.unescape_html(new_url))
parsed_url.query_values = (parsed_url.query_values || {}).merge({ instfs_id: new_instfs_ref })
file_metadata[:canvas_urls][url] = parsed_url.to_s
end
end
return render json: file_urls_with_uuids.to_json, status: :created if file_urls_with_uuids.present?

View File

@ -1783,6 +1783,20 @@ class Attachment < ActiveRecord::Base
end
end
def create_rce_reference(location, context: nil)
if instfs_hosted? && InstFS.enabled?
tenant_auth = { location: }
if context.present?
tenant_auth[:application] = Lti::Oauth2::AccessToken::ISS
tenant_auth[:context_type] = context.table_name
tenant_auth[:context_uuid] = context.uuid
tenant_auth[:account_uuid] = context.account.uuid
tenant_auth[:root_account_uuid] = context.root_account.uuid
end
InstFS.duplicate_file(instfs_uuid, tenant_auth:)
end
end
def self.file_removed_path
Rails.public_path.join("file_removed/file_removed.pdf")
end

View File

@ -265,8 +265,8 @@ module InstFS
json_response["success"][0]["id"]
end
def duplicate_file(instfs_uuid)
token = duplicate_file_jwt(instfs_uuid)
def duplicate_file(instfs_uuid, tenant_auth: nil)
token = duplicate_file_jwt(instfs_uuid, tenant_auth:)
url = "#{app_host}/files/#{instfs_uuid}/duplicate?token=#{token}"
response = CanvasHttp.post(url)
@ -462,12 +462,13 @@ module InstFS
SHORT_JWT_EXPIRATION)
end
def duplicate_file_jwt(instfs_uuid)
service_jwt({
iat: Time.now.utc.to_i,
resource: "/files/#{instfs_uuid}/duplicate"
},
SHORT_JWT_EXPIRATION)
def duplicate_file_jwt(instfs_uuid, tenant_auth: nil)
jwt_contents = {
iat: Time.now.utc.to_i,
resource: "/files/#{instfs_uuid}/duplicate"
}
jwt_contents[:tenant_auth] = tenant_auth if tenant_auth.present?
service_jwt(jwt_contents, SHORT_JWT_EXPIRATION)
end
def delete_file_jwt(instfs_uuid)

View File

@ -1687,6 +1687,7 @@ describe "Files API", type: :request do
account_admin_user(account: @course.root_account)
user_session(@user)
allow(Canvadocs).to receive(:enabled?).and_return(true)
allow(InstFS).to receive_messages(enabled?: true, app_host: "http://instfs.test")
end
it "returns 404 if feature not enabled" do
@ -1695,24 +1696,29 @@ describe "Files API", type: :request do
end
it "allows access to course files the user has access to manage" do
doc = attachment_model(context: @course, display_name: "test.docx", uploaded_data: fixture_file_upload("test.docx"), instfs_uuid: "doc")
image = attachment_model(context: @course, display_name: "cn_image.jpg", uploaded_data: fixture_file_upload("cn_image.jpg"), instfs_uuid: "image")
media = attachment_model(context: @course, display_name: "292.mp3", uploaded_data: fixture_file_upload("292.mp3"), instfs_uuid: "media")
course = @course
doc = attachment_model(context: course, display_name: "test.docx", uploaded_data: fixture_file_upload("test.docx"), instfs_uuid: "doc")
image = attachment_model(context: course, display_name: "cn_image.jpg", uploaded_data: fixture_file_upload("cn_image.jpg"), instfs_uuid: "image")
media = attachment_model(context: course, display_name: "292.mp3", uploaded_data: fixture_file_upload("292.mp3"), instfs_uuid: "media")
diff_course = attachment_model(context: course_factory, display_name: "292.mp3", uploaded_data: fixture_file_upload("292.mp3"), instfs_uuid: "media2")
file_urls = [
"/courses/#{@course.id}/files/#{doc.id}?wrap=1",
"/courses/#{@course.id}/files/#{image.id}/preview",
"/courses/#{course.id}/files/#{doc.id}?wrap=1",
"/courses/#{course.id}/files/#{image.id}/preview",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true",
"/media_attachments_iframe/#{diff_course.id}?type=video&amp;embedded=true"
]
body = { user_uuid: @teacher.uuid, file_urls:, location: "quiz/123" }
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
stub_request(:post, %r{^http://instfs.test/files/doc/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_doc_id" }.to_json)
stub_request(:post, %r{^http://instfs.test/files/media/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_media_id" }.to_json)
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
json = JSON.parse(response.body)
expect(json).to eq({
"instfs_ids" => { "/courses/#{@course.id}/files/#{image.id}/preview" => "image" },
"instfs_ids" => { "/courses/#{course.id}/files/#{image.id}/preview" => "image" },
"canvas_urls" => {
"/courses/#{@course.id}/files/#{doc.id}?wrap=1" => "/courses/#{@course.id}/files/#{doc.id}?wrap=1",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true"
"/courses/#{course.id}/files/#{doc.id}?wrap=1" => "/courses/#{course.id}/files/#{doc.id}?instfs_id=new_doc_id&wrap=1",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media.id}?embedded=true&instfs_id=new_media_id&type=video"
}
})
end
@ -1721,21 +1727,25 @@ describe "Files API", type: :request do
doc = attachment_model(context: @teacher, display_name: "test.docx", uploaded_data: fixture_file_upload("test.docx"), instfs_uuid: "doc")
image = attachment_model(context: @teacher, display_name: "cn_image.jpg", uploaded_data: fixture_file_upload("cn_image.jpg"), instfs_uuid: "image")
media = attachment_model(context: @teacher, display_name: "292.mp3", uploaded_data: fixture_file_upload("292.mp3"), instfs_uuid: "media")
not_yours = attachment_model(context: @user, display_name: "292.mp3", uploaded_data: fixture_file_upload("292.mp3"), instfs_uuid: "media")
file_urls = [
"/users/#{@teacher.id}/files/#{doc.id}?wrap=1",
"/users/#{@teacher.id}/files/#{image.id}/preview",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true",
"/media_attachments_iframe/#{not_yours.id}?type=video&amp;embedded=true"
]
body = { user_uuid: @teacher.uuid, file_urls:, location: "quiz/123" }
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
stub_request(:post, %r{^http://instfs.test/files/doc/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_doc_id" }.to_json)
stub_request(:post, %r{^http://instfs.test/files/media/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_media_id" }.to_json)
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
json = JSON.parse(response.body)
expect(json).to eq({
"instfs_ids" => { "/users/#{@teacher.id}/files/#{image.id}/preview" => "image" },
"canvas_urls" => {
"/users/#{@teacher.id}/files/#{doc.id}?wrap=1" => "/users/#{@teacher.id}/files/#{doc.id}?wrap=1",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true"
"/users/#{@teacher.id}/files/#{doc.id}?wrap=1" => "/users/#{@teacher.id}/files/#{doc.id}?instfs_id=new_doc_id&wrap=1",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media.id}?embedded=true&instfs_id=new_media_id&type=video"
}
})
end
@ -1745,14 +1755,15 @@ describe "Files API", type: :request do
file_urls = ["/files/#{doc.id}/download?download_frd=1", "/files/#{doc.id}", "http://example.canvas.edu/files/#{doc.id}/download"]
body = { user_uuid: @teacher.uuid, file_urls:, location: "quiz/123" }
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
stub_request(:post, %r{^http://instfs.test/files/doc/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_doc_id" }.to_json)
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
json = JSON.parse(response.body)
expect(json).to eq({
"canvas_urls" => {
"/files/#{doc.id}/download?download_frd=1" => "/files/#{doc.id}/download?download_frd=1",
"/files/#{doc.id}" => "/files/#{doc.id}",
"http://example.canvas.edu/files/#{doc.id}/download" => "http://example.canvas.edu/files/#{doc.id}/download"
"/files/#{doc.id}/download?download_frd=1" => "/files/#{doc.id}/download?download_frd=1&instfs_id=new_doc_id",
"/files/#{doc.id}" => "/files/#{doc.id}?instfs_id=new_doc_id",
"http://example.canvas.edu/files/#{doc.id}/download" => "http://example.canvas.edu/files/#{doc.id}/download?instfs_id=new_doc_id"
}
})
end
@ -1769,12 +1780,37 @@ describe "Files API", type: :request do
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true",
]
body = { user_uuid: @teacher.uuid, file_urls:, location: "quiz/123" }
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 422)
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 422)
json = JSON.parse(response.body)
expect(json).to eq({ "errors" => [{ "message" => "No valid file URLs given" }] })
end
it "shows hidden files" do
doc = attachment_model(context: @course, display_name: "test.docx", uploaded_data: fixture_file_upload("test.docx"), instfs_uuid: "doc", file_state: "hidden")
image = attachment_model(context: @course, display_name: "cn_image.jpg", uploaded_data: fixture_file_upload("cn_image.jpg"), instfs_uuid: "image", file_state: "hidden")
media = attachment_model(context: @course, display_name: "292.mp3", uploaded_data: fixture_file_upload("292.mp3"), instfs_uuid: "media", file_state: "hidden")
file_urls = [
"/courses/#{@course.id}/files/#{doc.id}?wrap=1",
"/courses/#{@course.id}/files/#{image.id}/preview",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true",
]
body = { user_uuid: @teacher.uuid, file_urls:, location: "quiz/123" }
stub_request(:post, %r{^http://instfs.test/files/doc/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_doc_id" }.to_json)
stub_request(:post, %r{^http://instfs.test/files/media/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_media_id" }.to_json)
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
json = JSON.parse(response.body)
expect(json).to eq({
"instfs_ids" => { "/courses/#{@course.id}/files/#{image.id}/preview" => "image" },
"canvas_urls" => {
"/courses/#{@course.id}/files/#{doc.id}?wrap=1" => "/courses/#{@course.id}/files/#{doc.id}?instfs_id=new_doc_id&wrap=1",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media.id}?embedded=true&instfs_id=new_media_id&type=video"
}
})
end
it "follows replaced files" do
doc2 = attachment_model(context: @course, display_name: "test.docx", uploaded_data: fixture_file_upload("test.docx"), instfs_uuid: "doc2")
image2 = attachment_model(context: @course, display_name: "cn_image.jpg", uploaded_data: fixture_file_upload("cn_image.jpg"), instfs_uuid: "image2")
@ -1791,14 +1827,16 @@ describe "Files API", type: :request do
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true",
]
body = { user_uuid: @teacher.uuid, file_urls:, location: "quiz/123" }
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
stub_request(:post, %r{^http://instfs.test/files/doc2/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_doc2_id" }.to_json)
stub_request(:post, %r{^http://instfs.test/files/media2/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_media2_id" }.to_json)
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
json = JSON.parse(response.body)
expect(json).to eq({
"instfs_ids" => { "/courses/#{@course.id}/files/#{image.id}/preview" => "image2" },
"canvas_urls" => {
"/courses/#{@course.id}/files/#{doc.id}?wrap=1" => "/courses/#{@course.id}/files/#{doc2.id}?wrap=1",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media2.id}?type=video&amp;embedded=true"
"/courses/#{@course.id}/files/#{doc.id}?wrap=1" => "/courses/#{@course.id}/files/#{doc2.id}?instfs_id=new_doc2_id&wrap=1",
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media2.id}?embedded=true&instfs_id=new_media2_id&type=video"
}
})
end
@ -1812,12 +1850,13 @@ describe "Files API", type: :request do
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true",
]
body = { user_uuid: @teacher.uuid, file_urls:, location: "quiz/123" }
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
stub_request(:post, %r{^http://instfs.test/files/media/duplicate\?token=}).to_return(status: 201, body: { "id" => "new_media_id" }.to_json)
api_call(:post, "/api/v1/rce_linked_file_urls", { controller: "files", action: "rce_linked_file_urls", format: "json" }, body, {}, expected_status: 201)
json = JSON.parse(response.body)
expect(json).to eq({
"canvas_urls" => {
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true"
"/media_attachments_iframe/#{media.id}?type=video&amp;embedded=true" => "/media_attachments_iframe/#{media.id}?embedded=true&instfs_id=new_media_id&type=video"
}
})
end