Skip to content

Commit 24ec2d5

Browse files
authored
Merge pull request #551 from wri/feature/authorize-uploads
Feature/authorize uploads
2 parents 8710cd0 + 2b5ec66 commit 24ec2d5

22 files changed

+646
-8
lines changed

.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ FRONTEND_URL=
1818
OBSERVATIONS_TOOL_URL=
1919

2020
SENTRY_DSN=
21+
SENTRY_LOG_UNAUTHORIZED_DOWNLOADS=true
2122

2223
SENDGRID_DOMAIN=
2324
SENDGRID_API_KEY=

.rubocop.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,3 @@ Rails/WhereRange:
6565

6666
Rails/Blank:
6767
Enabled: false
68-

app/controllers/uploads_controller.rb

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
class UploadsController < ApplicationController
44
rescue_from ActionController::MissingFile, with: :raise_not_found_exception
5-
rescue_from SecurityError, with: :raise_not_found_exception
6-
rescue_from CanCan::AccessDenied, with: :raise_not_found_exception
5+
rescue_from SecurityError, with: :log_and_raise_not_found_exception
6+
rescue_from CanCan::AccessDenied, with: :log_and_raise_not_found_exception
77

88
MODELS_OVERRIDES = {
99
"operator_document_file" => "document_file",
@@ -15,6 +15,7 @@ def download
1515
parse_upload_path
1616
ensure_valid_db_record
1717
track_download if trackable_request?
18+
check_authorization! if needs_authorization?
1819
send_file @sanitized_filepath, disposition: :inline
1920
end
2021

@@ -79,6 +80,29 @@ def ensure_valid_filename
7980
end
8081
end
8182

83+
def cookie_download_users
84+
cookies
85+
.select { |name, _v| name.ends_with?("download_user") }
86+
.map do |name, download_token|
87+
payload = Rails.application.message_verifier("download_token").verify(download_token)
88+
User.find_by(id: payload["user_id"])
89+
rescue ActiveSupport::MessageVerifier::InvalidSignature
90+
nil
91+
end.compact
92+
end
93+
94+
def check_authorization!
95+
raise SecurityError unless download_users.any? { it.can?(:download_protected, @record) }
96+
end
97+
98+
def download_users
99+
[current_user, *cookie_download_users].compact
100+
end
101+
102+
def needs_authorization?
103+
@uploader.protected?
104+
end
105+
82106
def allowed_models
83107
uploads_root = ApplicationUploader.new.root.join("uploads")
84108
Dir.entries(uploads_root)
@@ -151,7 +175,14 @@ def allowed_directory
151175
Rails.env.test? ? File.join(Rails.root, "tmp", "uploads") : File.join(Rails.root, "uploads")
152176
end
153177

178+
def log_and_raise_not_found_exception
179+
msg = "Unauthorized file download attempt: user_id=#{current_user&.id}, path=#{@sanitized_filepath}"
180+
Rails.logger.warn(msg)
181+
Sentry.capture_message(msg) if ENV["SENTRY_LOG_UNAUTHORIZED_DOWNLOADS"] == "true"
182+
raise_not_found_exception
183+
end
184+
154185
def raise_not_found_exception
155-
raise ActionController::RoutingError, "Not Found"
186+
raise ActionController::RoutingError, "Not found or your download session has expired (try clicking on the link again)"
156187
end
157188
end

app/controllers/v1/sessions_controller.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ module V1
44
class SessionsController < APIController
55
skip_before_action :authenticate, only: [:create]
66

7+
include ActionController::Cookies
8+
79
def create
810
@user = User.find_by(email: auth_params[:email])
911
if @user.present? && @user.valid_password?(auth_params[:password]) && @user.is_active
1012
token = Auth.issue({user: @user.id})
1113
@user.update_tracked_fields!(request)
14+
set_download_session_cookie_for(@user)
1215
render json: {token: token, role: @user.user_permission.user_role,
1316
user_id: @user.id, country: @user.country_id,
1417
operator_ids: @user.operator_ids, observer: @user.observer_id}, status: :ok
@@ -17,10 +20,38 @@ def create
1720
end
1821
end
1922

23+
def destroy
24+
cookies.delete(download_user_cookie_name)
25+
end
26+
27+
# each app, like portal and observation tool can have it's own download user cookie to prevent some edgecases
28+
def download_session
29+
set_download_session_cookie_for(current_user)
30+
head :ok
31+
end
32+
2033
private
2134

2235
def auth_params
2336
params.require(:auth).permit(:email, :password, :current_sign_in_ip)
2437
end
38+
39+
def set_download_session_cookie_for(user)
40+
download_token = Rails.application.message_verifier("download_token").generate(
41+
{user_id: user.id},
42+
expires_in: 10.minutes
43+
)
44+
cookies[download_user_cookie_name] = {
45+
value: download_token,
46+
expires: 10.minutes.from_now,
47+
same_site: :strict,
48+
secure: Rails.env.production? || Rails.env.staging?,
49+
httponly: true
50+
}
51+
end
52+
53+
def download_user_cookie_name
54+
[context[:app], "download_user"].compact.join("_")
55+
end
2556
end
2657
end

app/models/ability.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ def initialize(user = nil)
2929
end
3030
can :ru, Notification, user_id: user.id
3131
can :dismiss, Notification, user_id: user.id
32+
can :download_protected, OperatorDocumentAnnex do |annex|
33+
can? :update, annex.operator_document
34+
end
35+
can :download_protected, DocumentFile do |document_file|
36+
if document_file.owner.is_a?(OperatorDocumentHistory)
37+
can? :manage, document_file.owner.operator_document
38+
else
39+
can? :manage, document_file.owner
40+
end
41+
end
3242
else
3343
can [:read], User, id: user.id
3444
end

app/models/operator.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ def translated_types
121121
end
122122
end
123123

124+
def publication_authorization_signed?
125+
approved?
126+
end
127+
124128
def holding_users
125129
holding&.users || User.none
126130
end

app/models/operator_document.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ def publication_authorization?
139139
required_operator_document.contract_signature?
140140
end
141141

142+
# # TODO: I would name it public? but we have public attribute that should be refactored at some point
143+
def needs_authorization_before_downloading?
144+
return true if publication_authorization?
145+
return false if (doc_valid? || doc_expired?) && (operator.publication_authorization_signed? || public?)
146+
147+
true
148+
end
149+
142150
def name_with_fmu
143151
return required_operator_document.name if fmu.nil?
144152

app/models/operator_document_annex.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ class OperatorDocumentAnnex < ApplicationRecord
4545
scope :from_operator, ->(operator_id) { joins(:operator_document).where(operator_documents: {operator_id: operator_id}) }
4646
scope :orphaned, -> { where.not(id: AnnexDocument.select(:operator_document_annex_id)) }
4747

48+
def needs_authorization_before_downloading?
49+
return false if (doc_valid? || doc_expired?) && any_operator_document_without_authorization?
50+
51+
true
52+
end
53+
4854
def self.expire_document_annexes
4955
today = Time.zone.today
5056
documents_to_expire =
@@ -61,4 +67,10 @@ def operator_document_name
6167
def expire_document_annex
6268
update(status: OperatorDocumentAnnex.statuses[:doc_expired])
6369
end
70+
71+
private
72+
73+
def any_operator_document_without_authorization?
74+
[operator_document, *operator_document_histories].compact.any? { !it.needs_authorization_before_downloading? }
75+
end
6476
end

app/models/operator_document_history.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ def publication_authorization?
5656
required_operator_document.contract_signature?
5757
end
5858

59+
def needs_authorization_before_downloading?
60+
return true if publication_authorization?
61+
return false if (doc_valid? || doc_expired?) && (operator.publication_authorization_signed? || public?)
62+
63+
true
64+
end
65+
5966
# Returns the collection of OperatorDocumentHistory for a given operator at a point in time
6067
#
6168
# @param String operator_id The operator id

app/models/user.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ class User < ApplicationRecord
9090
scope :inactive, -> { where(is_active: false) }
9191
scope :with_roles, ->(role) { joins(:user_permission).where(user_permission: {user_role: role}) }
9292

93+
delegate :can?, :cannot, to: :ability
94+
def ability
95+
@ability ||= Ability.new(self)
96+
end
97+
9398
def self.ransackable_attributes(auth_object = nil)
9499
%w[name first_name last_name email id created_at]
95100
end

0 commit comments

Comments
 (0)