Skip to content

Commit

Permalink
Merge pull request #15342 from opf/implementation/53996-use-authentic…
Browse files Browse the repository at this point in the history
…ation-in-create_folder_command

[#53996] use auth strategies in create folder commands
  • Loading branch information
Kharonus committed Apr 26, 2024
2 parents 1d6701b + e30e811 commit 5c0fd39
Show file tree
Hide file tree
Showing 59 changed files with 7,716 additions and 7,972 deletions.
Expand Up @@ -28,61 +28,65 @@
# See COPYRIGHT and LICENSE files for more details.
#++

module Storages::Peripherals
# rubocop:disable Lint/EmptyClass
class UnknownSource; end

# rubocop:enable Lint/EmptyClass

module ServiceResultRefinements
refine ServiceResult do
def match(on_success:, on_failure:)
if success?
on_success.call(result)
else
on_failure.call(errors)
module Storages
module Peripherals
# rubocop:disable Lint/EmptyClass
class UnknownSource; end

# rubocop:enable Lint/EmptyClass

module ServiceResultRefinements
refine ServiceResult do
def match(on_success:, on_failure:)
if success?
on_success.call(result)
else
on_failure.call(errors)
end
end
end

def bind
return self if failure?
def bind
return self if failure?

yield result
end
yield result
end

def >>(other)
unless other.respond_to?(:call)
raise TypeError, "Expected an object responding to 'call', got #{other.class.name}."
end

def >>(other)
unless other.respond_to?(:call)
raise TypeError, "Expected an object responding to 'call', got #{other.class.name}."
bind(&other)
end

bind(&other)
end
def error_source
if errors.is_a?(::Storages::StorageError) && errors.data&.source.present?
errors.data.source
else
UnknownSource
end
end

def error_source
if errors.is_a?(::Storages::StorageError) && errors.data&.source.present?
errors.data.source
else
UnknownSource
def error_payload
errors.data&.payload
end
end

def error_payload
errors.data&.payload
end
def result_or
return result if success?

def result_or
return result if success?
yield errors
end

yield errors
end
alias_method :error_and, :result_or
alias_method :error_and, :result_or

def result_and
return errors if failure?

def result_and
return errors if failure?
yield result
end

yield result
alias_method :error_or, :result_and
end
alias_method :error_or, :result_and
end
end
end
Expand Up @@ -37,15 +37,16 @@ def self.strategy
Strategy.new(:oauth_client_credentials)
end

# rubocop:disable Metrics/AbcSize
def call(storage:, http_options: {})
config = storage.oauth_configuration.to_httpx_oauth_config

return build_failure(storage) unless config.valid?

access_token = Rails.cache.read("storage.#{storage.id}.access_token")
access_token = Rails.cache.read("storage.#{storage.id}.httpx_access_token")

http_result = if access_token.present?
http_with_current_token(access_token: access_token, http_options: http_options)
http_with_current_token(access_token:, http_options:)
else
http_with_new_token(issuer: config.issuer,
client_id: config.client_id,
Expand All @@ -55,16 +56,19 @@ def call(storage:, http_options: {})
end

return http_result if http_result.failure?

http = http_result.result

if access_token.nil?
token = http.instance_variable_get(:@options).oauth_session.access_token
Rails.cache.write("storage.#{storage.id}.access_token", token, expires_in: 50.minutes)
Rails.cache.write("storage.#{storage.id}.httpx_access_token", token, expires_in: 50.minutes)
end

yield http
end

# rubocop:enable Metrics/AbcSize

private

def http_with_current_token(access_token:, http_options:)
Expand All @@ -83,9 +87,9 @@ def http_with_new_token(issuer:, client_id:, client_secret:, scope:, http_option
.with(http_options)
ServiceResult.success(result: http)
rescue HTTPX::HTTPError => e
return Failures::Builder.call(code: :unauthorized,
log_message: "Error while fetching OAuth access token.",
data: Failures::ErrorData.call(response: e.response, source: self.class))
Failures::Builder.call(code: :unauthorized,
log_message: "Error while fetching OAuth access token.",
data: Failures::ErrorData.call(response: e.response, source: self.class))
end

def build_failure(storage)
Expand Down
Expand Up @@ -28,56 +28,114 @@
# See COPYRIGHT and LICENSE files for more details.
#++

module Storages::Peripherals::StorageInteraction::Nextcloud
class CreateFolderCommand
using Storages::Peripherals::ServiceResultRefinements

def initialize(storage)
@uri = storage.uri
@username = storage.username
@password = storage.password
end
module Storages
module Peripherals
module StorageInteraction
module Nextcloud
class CreateFolderCommand
using ServiceResultRefinements

def self.call(storage:, folder_path:)
new(storage).call(folder_path:)
end
def self.call(storage:, auth_strategy:, folder_name:, parent_location:)
new(storage).call(auth_strategy:, folder_name:, parent_location:)
end

def initialize(storage)
@storage = storage
end

def call(auth_strategy:, folder_name:, parent_location:)
origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:)
if origin_user_id.failure?
return origin_user_id
end

folder_path = Util.join_uri_path(parent_location, folder_name)
path_prefix = URI.parse(Util.join_uri_path(base_uri, CGI.escapeURIComponent(origin_user_id.result))).path
request_url = Util.join_uri_path(base_uri,
CGI.escapeURIComponent(origin_user_id.result),
Util.escape_path(folder_path))

create_folder_request(auth_strategy, request_url, path_prefix)
end

private

def create_folder_request(auth_strategy, request_url, path_prefix)
Authentication[auth_strategy].call(storage: @storage) do |http|
result = handle_response(http.mkcol(request_url))
return result if result.failure?

# rubocop:disable Metrics/AbcSize
def call(folder_path:)
response = OpenProject
.httpx
.basic_auth(@username, @password)
.mkcol(
Util.join_uri_path(
@uri,
"remote.php/dav/files",
CGI.escapeURIComponent(@username),
Util.escape_path(folder_path)
)
)

error_data = Storages::StorageErrorData.new(source: self.class, payload: response)

case response
in { status: 200..299 }
ServiceResult.success(message: "Folder was successfully created.")
in { status: 405 }
if Util.error_text_from_response(response) == "The resource you tried to create already exists"
ServiceResult.success(message: "Folder already exists.")
else
Util.error(:not_allowed, "Outbound request method not allowed", error_data)
handle_response(http.propfind(request_url, requested_properties)).map do |response|
storage_file(path_prefix, response)
end
end
end

def handle_response(response)
case response
in { status: 200..299 }
ServiceResult.success(result: response)
in { status: 401 }
Util.failure(code: :unauthorized,
data: Util.error_data_from_response(caller: self.class, response:),
log_message: "Outbound request not authorized!")
in { status: 404 | 409 } # webDAV endpoint returns 409 if path does not exist
Util.failure(code: :not_found,
data: Util.error_data_from_response(caller: self.class, response:),
log_message: "Outbound request destination not found!")
in { status: 405 } # webDAV endpoint returns 405 if folder already exists
Util.failure(code: :conflict,
data: Util.error_data_from_response(caller: self.class, response:),
log_message: "Folder already exists")
else
Util.failure(code: :error,
data: Util.error_data_from_response(caller: self.class, response:),
log_message: "Outbound request failed with unknown error!")
end
end

def requested_properties
Nokogiri::XML::Builder.new do |xml|
xml["d"].propfind(
"xmlns:d" => "DAV:",
"xmlns:oc" => "http://owncloud.org/ns"
) do
xml["d"].prop do
xml["oc"].fileid
xml["oc"].size
xml["d"].getlastmodified
xml["oc"].send(:"owner-display-name")
end
end
end.to_xml
end

# rubocop:disable Metrics/AbcSize
def storage_file(path_prefix, response)
xml = response.xml
path = xml.xpath("//d:response/d:href/text()").to_s
timestamp = xml.xpath("//d:response/d:propstat/d:prop/d:getlastmodified/text()").to_s
creator = xml.xpath("//d:response/d:propstat/d:prop/oc:owner-display-name/text()").to_s
location = CGI.unescapeURIComponent(path.gsub(path_prefix, "")).delete_suffix("/")

StorageFile.new(
id: xml.xpath("//d:response/d:propstat/d:prop/oc:fileid/text()").to_s,
name: location.split("/").last,
size: xml.xpath("//d:response/d:propstat/d:prop/oc:size/text()").to_s,
mime_type: "application/x-op-directory",
created_at: Time.zone.parse(timestamp),
last_modified_at: Time.zone.parse(timestamp),
created_by_name: creator,
last_modified_by_name: creator,
location:
)
end

# rubocop:enable Metrics/AbcSize

def base_uri = Util.join_uri_path(@storage.uri, "remote.php", "dav", "files")
end
in { status: 401 }
Util.error(:unauthorized, "Outbound request not authorized", error_data)
in { status: 404 }
Util.error(:not_found, "Outbound request destination not found", error_data)
in { status: 409 }
Util.error(:conflict, Util.error_text_from_response(response), error_data)
else
Util.error(:error, "Outbound request failed", error_data)
end
end

# rubocop:enable Metrics/AbcSize
end
end

0 comments on commit 5c0fd39

Please sign in to comment.