Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/controllers/api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require "auth"

class APIController < ActionController::API
class UnprocessableContentError < StandardError; end

include CanCan::ControllerAdditions
include JSONAPI::ActsAsResourceController

Expand All @@ -23,6 +25,7 @@ def context
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
rescue_from ActionController::RoutingError, with: :record_not_found
rescue_from JWT::VerificationError, with: :bad_auth_key
rescue_from UnprocessableContentError, with: :unprocessable_content

rescue_from CanCan::AccessDenied do |exception|
Rails.logger.debug { "Access denied on #{exception.action} #{exception.subject.inspect}" }
Expand Down Expand Up @@ -81,6 +84,10 @@ def record_not_found
render json: {errors: [{status: 404, title: "Record not found"}]}, status: :not_found
end

def unprocessable_content(exception)
render json: {errors: [{status: 422, title: exception.message}]}, status: :unprocessable_content
end

def render_unprocessable_entity_error(errors)
json_errors = {errors: []}

Expand Down
33 changes: 32 additions & 1 deletion app/resources/concerns/operator_documentable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ module OperatorDocumentable
:status, :created_at, :updated_at,
:attachment, :operator_id, :required_operator_document_id,
:fmu_id, :uploaded_by, :reason, :response_date,
:public, :source_info, :admin_comment
:public, :source_info, :admin_comment,
:source_operator_document_id, :source_annex_id
attribute :source_type, delegate: :source

has_one :country
Expand Down Expand Up @@ -59,6 +60,32 @@ def attachment=(attachment)
@model.new_document_uploaded = true
end

def source_operator_document_id
nil
end

def source_operator_document_id=(id)
source = OperatorDocument.find(id)
unless current_user_operator_ids.include?(source.operator_id)
raise APIController::UnprocessableContentError, "source-operator-document-id must belong to your operator"
end

self.attachment = File.open(source.document_file.attachment.path)
end

def source_annex_id
nil
end

def source_annex_id=(id)
source = OperatorDocumentAnnex.find(id)
unless current_user_operator_ids.include?(source.operator_document&.operator_id)
raise APIController::UnprocessableContentError, "source-annex-id must belong to your operator"
end

self.attachment = File.open(source.attachment.path)
end

def document_visible?
can_see_document? || document_public?
end
Expand All @@ -79,6 +106,10 @@ def can_see_document?

false
end

def current_user_operator_ids
@context[:current_user]&.operator_ids || []
end
end

module ClassMethods
Expand Down
40 changes: 36 additions & 4 deletions app/resources/v1/operator_document_annex_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ class OperatorDocumentAnnexResource < BaseResource
include Privateable

caching
attributes :name,
:start_date, :expire_date, :status, :invalidation_reason, :attachment,
:uploaded_by, :created_at, :updated_at
attributes :name, :start_date, :expire_date, :status, :invalidation_reason, :attachment,
:uploaded_by, :created_at, :updated_at,
:source_operator_document_id, :source_annex_id

privateable :show_attributes?, [:name, :invalidation_reason, :start_date, :expire_date, :status, :attachment, :uploaded_by, :created_at, :updated_at]

Expand All @@ -21,13 +21,15 @@ class OperatorDocumentAnnexResource < BaseResource
before_create :set_user_id, :set_status_pending, :set_public

def self.updatable_fields(context)
[:name, :start_date, :expire_date, :attachment]
[:name, :start_date, :expire_date, :attachment, :source_operator_document_id, :source_annex_id]
end

def self.creatable_fields(context)
updatable_fields(context) + [:operator_document]
end

delegate :attachment=, to: :@model

def operator_document_id=(operator_document_id)
od = OperatorDocument.find operator_document_id
@model.operator_document = od # this will also set @model.annex_document
Expand All @@ -37,6 +39,32 @@ def operator_document_id=(operator_document_id)
@model.annex_documents_history << adh
end

def source_operator_document_id
nil
end

def source_operator_document_id=(id)
source = OperatorDocument.find(id)
unless current_user_operator_ids.include?(source.operator_id)
raise APIController::UnprocessableContentError, "source-operator-document-id must belong to your operator"
end

self.attachment = File.open(source.document_file.attachment.path)
end

def source_annex_id
nil
end

def source_annex_id=(id)
source = OperatorDocumentAnnex.find(id)
unless current_user_operator_ids.include?(source.operator_document&.operator_id)
raise APIController::UnprocessableContentError, "source-annex-id must belong to your operator"
end

self.attachment = File.open(source.attachment.path)
end

def set_user_id
if context[:current_user].present?
@model.user_id = context[:current_user].id
Expand Down Expand Up @@ -84,5 +112,9 @@ def belongs_to_user?

user.is_operator?(@model.operator&.id)
end

def current_user_operator_ids
context[:current_user]&.operator_ids || []
end
end
end
97 changes: 95 additions & 2 deletions config/brakeman.ignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,97 @@
{
"ignored_warnings": [],
"brakeman_version": "7.1.0"
"ignored_warnings": [
{
"warning_type": "File Access",
"warning_code": 16,
"fingerprint": "024f9c6c74d2f952c245a67b12de951edc532e5d570da663e2da9fe004a2c8ba",
"check_name": "FileAccess",
"message": "Model attribute used in file name",
"file": "app/resources/concerns/operator_documentable.rb",
"line": 73,
"link": "https://brakemanscanner.org/docs/warning_types/file_access/",
"code": "File.open(OperatorDocument.find(id).document_file.attachment.path)",
"render_path": null,
"location": {
"type": "method",
"class": "OperatorDocumentable",
"method": "source_operator_document_id="
},
"user_input": "OperatorDocument.find(id).document_file.attachment.path",
"confidence": "Medium",
"cwe_id": [
22
],
"note": ""
},
{
"warning_type": "File Access",
"warning_code": 16,
"fingerprint": "029b1df6a60878f89705b53f34359caa764d990e011d24ac1bd22a263c66e323",
"check_name": "FileAccess",
"message": "Model attribute used in file name",
"file": "app/resources/v1/operator_document_annex_resource.rb",
"line": 44,
"link": "https://brakemanscanner.org/docs/warning_types/file_access/",
"code": "File.open(OperatorDocument.find(id).document_file.attachment.path)",
"render_path": null,
"location": {
"type": "method",
"class": "V1::OperatorDocumentAnnexResource",
"method": "source_operator_document_id="
},
"user_input": "OperatorDocument.find(id).document_file.attachment.path",
"confidence": "Medium",
"cwe_id": [
22
],
"note": ""
},
{
"warning_type": "File Access",
"warning_code": 16,
"fingerprint": "02f13e5f55adc59afe435b7296349aaeee025c456004334f0f70925597a3132f",
"check_name": "FileAccess",
"message": "Model attribute used in file name",
"file": "app/resources/concerns/operator_documentable.rb",
"line": 86,
"link": "https://brakemanscanner.org/docs/warning_types/file_access/",
"code": "File.open(OperatorDocumentAnnex.find(id).attachment.path)",
"render_path": null,
"location": {
"type": "method",
"class": "OperatorDocumentable",
"method": "source_annex_id="
},
"user_input": "OperatorDocumentAnnex.find(id).attachment.path",
"confidence": "Medium",
"cwe_id": [
22
],
"note": ""
},
{
"warning_type": "File Access",
"warning_code": 16,
"fingerprint": "49345b1c4e5bd083953300f9545745fbc0dd77f23f7070aba71cd6f04cc0011d",
"check_name": "FileAccess",
"message": "Model attribute used in file name",
"file": "app/resources/v1/operator_document_annex_resource.rb",
"line": 57,
"link": "https://brakemanscanner.org/docs/warning_types/file_access/",
"code": "File.open(OperatorDocumentAnnex.find(id).attachment.path)",
"render_path": null,
"location": {
"type": "method",
"class": "V1::OperatorDocumentAnnexResource",
"method": "source_annex_id="
},
"user_input": "OperatorDocumentAnnex.find(id).attachment.path",
"confidence": "Medium",
"cwe_id": [
22
],
"note": ""
}
],
"brakeman_version": "8.0.4"
}
2 changes: 1 addition & 1 deletion config/initializers/jsonapi_resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
config.always_include_to_one_linkage_data = false
config.warn_on_missing_routes = false
config.default_exclude_links = :default
config.exception_class_whitelist = [CanCan::AccessDenied]
config.exception_class_whitelist = [CanCan::AccessDenied, "APIController::UnprocessableContentError"]

# Metadata
# Output record count in top level meta for find operation
Expand Down
71 changes: 71 additions & 0 deletions spec/integration/v1/operator_document_annexes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,75 @@ module V1
end
end
end

describe "File reuse" do
let(:operator_document) { create(:operator_document_fmu, operator: operator_user.operator) }
let(:source_annex) { create(:operator_document_annex, operator_document: operator_document) }
let(:other_operator_document) { create(:operator_document_fmu) }
let(:other_source_annex) { create(:operator_document_annex, operator_document: other_operator_document) }

let(:base_params) {
{
name: "Reused annex",
"start-date": Time.zone.today.to_s,
relationships: {"operator-document": operator_document.id}
}
}

describe "source-operator-document-id" do
context "when same operator" do
it "creates annex copying attachment from the operator document" do
post(
"/operator-document-annexes",
params: jsonapi_params("operator-document-annexes", nil, base_params.merge("source-operator-document-id": operator_document.id)),
headers: operator_user_headers
)

expect(status).to eq(201)
expect(OperatorDocumentAnnex.find(parsed_data[:id]).attachment_identifier).to be_present
end
end

context "when different operator" do
it "returns 422 with error message" do
post(
"/operator-document-annexes",
params: jsonapi_params("operator-document-annexes", nil, base_params.merge("source-operator-document-id": other_operator_document.id)),
headers: operator_user_headers
)

expect(status).to eq(422)
expect(parsed_body[:errors].first[:title]).to include("must belong to your operator")
end
end
end

describe "source-annex-id" do
context "when same operator" do
it "creates annex copying attachment from another annex" do
post(
"/operator-document-annexes",
params: jsonapi_params("operator-document-annexes", nil, base_params.merge("source-annex-id": source_annex.id)),
headers: operator_user_headers
)

expect(status).to eq(201)
expect(OperatorDocumentAnnex.find(parsed_data[:id]).attachment_identifier).to be_present
end
end

context "when different operator" do
it "returns 422 with error message" do
post(
"/operator-document-annexes",
params: jsonapi_params("operator-document-annexes", nil, base_params.merge("source-annex-id": other_source_annex.id)),
headers: operator_user_headers
)

expect(status).to eq(422)
expect(parsed_body[:errors].first[:title]).to include("must belong to your operator")
end
end
end
end
end
Loading
Loading