Skip to content

Commit 268c9e1

Browse files
Rework "open storage" action
The previous implementation was a deeply nested decision tree for different error cases or other unexpected states. Most of this was necessary, because setting up correct permissions is partially happening asynchronously. Some of the redirects would eventually have rendered a modal that would periodically check whether user permissions appeared in the background. All of this was replaced by an approach where we immediately (synchronously) ensure that the setup is correct.
1 parent cefe0c4 commit 268c9e1

18 files changed

+783
-575
lines changed

modules/storages/app/common/storages/injector.rb

-35
This file was deleted.

modules/storages/app/common/storages/peripherals/nextcloud_registry.rb

+5
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ module Peripherals
8585
register(:managed_folder_identifier, ManagedFolderIdentifier::Nextcloud)
8686
end
8787

88+
namespace("services") do
89+
register(:folder_create, ::Storages::NextcloudManagedFolderCreateService)
90+
register(:folder_permissions, ::Storages::NextcloudManagedFolderPermissionsService)
91+
end
92+
8893
namespace("validators") do
8994
register(:connection, ConnectionValidators::NextcloudValidator)
9095
end

modules/storages/app/common/storages/peripherals/one_drive_registry.rb

+5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ module Peripherals
7676
register(:managed_folder_identifier, ManagedFolderIdentifier::OneDrive)
7777
end
7878

79+
namespace("services") do
80+
register(:folder_create, ::Storages::OneDriveManagedFolderCreateService)
81+
register(:folder_permissions, ::Storages::OneDriveManagedFolderPermissionsService)
82+
end
83+
7984
namespace("validators") do
8085
register(:connection, ConnectionValidators::OneDriveValidator)
8186
end

modules/storages/app/controllers/storages/project_storages_controller.rb

+74-87
Original file line numberDiff line numberDiff line change
@@ -40,110 +40,97 @@ class Storages::ProjectStoragesController < ApplicationController
4040
before_action :render_403, unless: -> { User.current.allowed_in_project?(:view_file_links, @project) }
4141
no_authorization_required! :open
4242

43-
# rubocop:disable Metrics/AbcSize
43+
before_action :ensure_remote_identity, only: :open
44+
before_action :ensure_folder_created, only: :open
45+
before_action :ensure_folder_permissions, only: :open
46+
4447
def open
45-
if @object.project_folder_automatic?
46-
@storage = @object.storage
47-
# check if user "see" project_folder
48-
if @object.project_folder_id.present?
49-
::Storages::Peripherals::Registry
50-
.resolve("#{@storage}.queries.file_info")
51-
.call(storage: @storage, auth_strategy:, file_id: @object.project_folder_id)
52-
.match(
53-
on_success: user_can_read_project_folder,
54-
on_failure: user_can_not_read_project_folder
55-
)
56-
else
57-
respond_to do |format|
58-
format.turbo_stream { head :no_content }
59-
format.html do
60-
redirect_to_project_overview_with_modal
61-
end
62-
end
63-
end
64-
else
65-
redirect_to api_v3_project_storage_open
66-
end
48+
@project_storage.open(current_user).match(
49+
on_success: ->(url) { redirect_to url, allow_other_host: true },
50+
on_failure: ->(error) { show_error(error.to_s) }
51+
)
6752
end
6853

69-
# rubocop:enable Metrics/AbcSize
70-
7154
private
7255

73-
def auth_strategy
74-
Storages::Peripherals::Registry.resolve("#{@storage}.authentication.user_bound").call(user: current_user, storage: @storage)
75-
end
56+
def ensure_remote_identity
57+
selector = Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector.new(user: current_user, storage:)
58+
return unless selector.storage_oauth?
7659

77-
def user_can_read_project_folder
78-
->(_) do
79-
respond_to do |format|
80-
format.turbo_stream do
81-
render(
82-
turbo_stream: OpTurbo::StreamComponent.new(
83-
action: :update,
84-
target: Storages::OpenProjectStorageModalComponent.dialog_body_id,
85-
template: Storages::OpenProjectStorageModalComponent::Body.new(:success).render_in(view_context)
86-
).render_in(view_context)
87-
)
88-
end
89-
format.html { redirect_to api_v3_project_storage_open }
90-
end
60+
case Storages::Peripherals::StorageInteraction::Authentication.authorization_state(storage:, user: current_user)
61+
when :not_connected, :failed_authorization
62+
redirect_to ensure_connection_url
63+
when :error
64+
show_error(I18n.t("project_storages.open.remote_identity_error"))
9165
end
9266
end
9367

94-
def user_can_not_read_project_folder
95-
->(result) do
96-
respond_to do |format|
97-
format.turbo_stream { head :no_content }
98-
format.html do
99-
case result.code
100-
when :unauthorized
101-
redirect_to(storage_fallback_url, allow_other_host: true)
102-
when :forbidden
103-
redirect_to_project_overview_with_modal
104-
end
105-
end
106-
end
68+
def ensure_folder_created
69+
return unless @project_storage.project_folder_automatic?
70+
return if @project_storage.project_folder_id.present?
71+
72+
folder_create_service.new(storage:, project_storages: project_storage_scope).call.on_failure do |result|
73+
return show_error(result.errors.full_messages)
10774
end
75+
@project_storage.reload
10876
end
10977

110-
def storage_fallback_url
111-
selector = Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector.new(user: current_user, storage: @storage)
112-
if selector.sso?
113-
# Maybe the user just can't read folder because they are not provisioned in (Nextcloud) storage. We redirect them
114-
# to the storage and leave error handling up to storage. Ideally they will login to the storage and thus prevent
115-
# the same error in the future.
116-
# This would not work for OneDrive, but for OneDrive we don't have SSO (yet).
117-
res = Storages::Peripherals::Registry.resolve("#{@storage}.queries.open_file_link").call(
118-
storage: @storage,
119-
auth_strategy:,
120-
file_id: @object.project_folder_id
121-
)
122-
res.result_or { |errors| raise "Could not redirect SSO user to storage: #{errors}" }
123-
else
124-
oauth_clients_ensure_connection_url(
125-
oauth_client_id: @storage.oauth_client.client_id,
126-
storage_id: @storage.id,
127-
destination_url: request.url
128-
)
78+
def ensure_folder_permissions
79+
return unless @project_storage.project_folder_automatic?
80+
81+
result = test_folder_access
82+
return if result.success? || result.errors.code != :forbidden
83+
84+
# Note: The time this operation takes may still scale with the number of users.
85+
# If this becomes a problem, we will have to update downstream code to allow changing permissions for a few users instead of
86+
# requiring to always set all permissions.
87+
folder_permissions_service.new(storage:, project_storages: project_storage_scope).call.on_failure do |r|
88+
return show_error(r.errors.full_messages)
12989
end
13090
end
13191

132-
def redirect_to_project_overview_with_modal
133-
redirect_to(
134-
project_overview_path(project_id: @project.identifier),
135-
op_modal: {
136-
component: Storages::OpenProjectStorageModalComponent.name,
137-
parameters: {
138-
project_storage_open_url: request.path,
139-
redirect_url: api_v3_project_storage_open,
140-
state: :waiting
141-
}
142-
}
92+
def ensure_connection_url
93+
oauth_clients_ensure_connection_url(
94+
oauth_client_id: storage.oauth_client.client_id,
95+
storage_id: storage.id,
96+
destination_url: request.url
97+
)
98+
end
99+
100+
def folder_create_service
101+
Storages::Peripherals::Registry.resolve("#{storage}.services.folder_create")
102+
end
103+
104+
def folder_permissions_service
105+
Storages::Peripherals::Registry.resolve("#{storage}.services.folder_permissions")
106+
end
107+
108+
def file_info
109+
Storages::Peripherals::Registry.resolve("#{storage}.queries.file_info")
110+
end
111+
112+
def user_bound
113+
Storages::Peripherals::Registry.resolve("#{storage}.authentication.user_bound")
114+
end
115+
116+
def storage
117+
@object.storage
118+
end
119+
120+
def project_storage_scope
121+
Storages::ProjectStorage.where(id: @project_storage.id)
122+
end
123+
124+
def test_folder_access
125+
file_info.call(
126+
storage:,
127+
auth_strategy: user_bound.call(storage:, user: current_user),
128+
file_id: @project_storage.project_folder_id
143129
)
144130
end
145131

146-
def api_v3_project_storage_open
147-
::API::V3::Utilities::PathHelper::ApiV3Path.project_storage_open(@object.id)
132+
def show_error(message)
133+
flash[:error] = message
134+
redirect_back(fallback_location: project_path(id: @project_storage.project_id))
148135
end
149136
end

modules/storages/app/models/storages/project_storage.rb

+4-9
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def open(user)
9797

9898
def open_with_connection_ensured
9999
return unless storage.configured?
100-
return open_project_storage_url if storage.authenticate_via_idp?
100+
return open_project_storage_url if storage.authenticate_via_idp? # TODO: use auth method selector
101101

102102
OpenProject::StaticRouting::StaticRouter.new.url_helpers.oauth_clients_ensure_connection_path(
103103
oauth_client_id: storage.oauth_client.client_id,
@@ -106,17 +106,12 @@ def open_with_connection_ensured
106106
)
107107
end
108108

109-
private
110-
111109
def open_project_storage_url
112-
OpenProject::StaticRouting::StaticRouter
113-
.new
114-
.url_helpers.open_project_storage_url(host: Setting.host_name,
115-
protocol: "https",
116-
project_id: project.identifier,
117-
id:)
110+
OpenProject::StaticRouting::StaticRouter.new.url_helpers.open_project_storage_url(project_id: project.identifier, id:)
118111
end
119112

113+
private
114+
120115
def managed_folder_identifier
121116
@managed_folder_identifier ||=
122117
Peripherals::Registry.resolve("#{storage}.models.managed_folder_identifier").new(self)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
module Storages
32+
class ManagedFolderSyncService < BaseService
33+
using Peripherals::ServiceResultRefinements
34+
35+
class << self
36+
def call(storage)
37+
new(storage).call
38+
end
39+
end
40+
41+
def initialize(storage)
42+
super()
43+
@storage = storage
44+
end
45+
46+
def call
47+
with_tagged_logger([self.class.name, "storage-#{@storage.id}"]) do
48+
info "Starting AMPF Sync for Storage #{@storage.id}"
49+
prepare_remote_folders.on_failure { return epilogue }
50+
apply_permissions_to_folders
51+
epilogue
52+
end
53+
end
54+
55+
private
56+
57+
def epilogue
58+
info "Synchronization process for Storage #{@storage.id} has ended. #{@result.errors.count} errors found."
59+
@result
60+
end
61+
62+
def prepare_remote_folders
63+
folder_create_service.new(storage: @storage).call.tap do |subresult|
64+
@result.merge!(subresult)
65+
end
66+
end
67+
68+
def apply_permissions_to_folders
69+
folder_permissions_service.new(storage: @storage).call.tap do |subresult|
70+
@result.merge!(subresult)
71+
end
72+
end
73+
74+
def folder_create_service
75+
Peripherals::Registry.resolve("#{@storage}.services.folder_create")
76+
end
77+
78+
def folder_permissions_service
79+
Peripherals::Registry.resolve("#{@storage}.services.folder_permissions")
80+
end
81+
end
82+
end

0 commit comments

Comments
 (0)