mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Allow to configure authentication method through UI
This includes a refactoring to our storage contracts, which so far required to perform all validations of a storage at once, even though the storage is assembled step by step. It's now possible to validate only one step of the storage creation, while still having a contract that validates the storage as a whole. By default the Create/Update contract will validate the whole storage, as before, but they can now be instructed to validate using one of the partial contracts.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
##
|
||||
# A contract that allows to compose itself from other contracts like:
|
||||
#
|
||||
# class MyContract < ComposedContract
|
||||
# include_contract MyOtherContractA
|
||||
# include_contract MyOtherContractB
|
||||
# end
|
||||
#
|
||||
# This allows to run the smaller contracts separately, but also to have one contract
|
||||
# that validates the model as a whole. The composed contract will make sure that all attributes
|
||||
# defined as writable in one contract are also considered writable in other contracts.
|
||||
#
|
||||
# It's not possible to add validations to the composed contract directly. If you need additional validations,
|
||||
# extract them into a separate contract that you include in the composed contract.
|
||||
class ComposedContract
|
||||
class << self
|
||||
def included_contract_classes
|
||||
@included_contract_classes ||= []
|
||||
end
|
||||
|
||||
def include_contract(contract_class)
|
||||
included_contract_classes << contract_class
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :errors, :model, :writable_attributes
|
||||
|
||||
def initialize(model, user, options: {})
|
||||
@model = model
|
||||
@user = user
|
||||
@options = options
|
||||
|
||||
@errors = ActiveModel::Errors.new(model)
|
||||
@writable_attributes = []
|
||||
end
|
||||
|
||||
def validate
|
||||
errors.clear
|
||||
included_contracts.each do |subcontract|
|
||||
subcontract.validate
|
||||
errors.merge!(subcontract.errors)
|
||||
end
|
||||
|
||||
errors.empty?
|
||||
end
|
||||
|
||||
def valid?
|
||||
validate
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def included_contracts
|
||||
@included_contracts ||= begin
|
||||
contracts = self.class.included_contract_classes.map { |klass| klass.new(model, @user, options: @options) }
|
||||
all_writable_attributes = (writable_attributes + contracts.flat_map(&:writable_attributes)).uniq
|
||||
contracts.each do |subcontract|
|
||||
subcontract.writable_attributes.append(*all_writable_attributes)
|
||||
end
|
||||
|
||||
contracts
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -61,6 +61,7 @@ module Storages
|
||||
namespace("forms") do
|
||||
register(:automatically_managed_folders, ::Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent)
|
||||
register(:general_information, ::Storages::Admin::Forms::GeneralInfoFormComponent)
|
||||
register(:nextcloud_audience, ::Storages::Admin::Forms::NextcloudAudienceFormComponent)
|
||||
register(:oauth_application, ::Storages::Admin::OAuthApplicationInfoCopyComponent)
|
||||
register(:oauth_client, ::Storages::Admin::Forms::OAuthClientFormComponent)
|
||||
end
|
||||
@@ -69,12 +70,15 @@ module Storages
|
||||
|
||||
register(:automatically_managed_folders, ::Storages::Admin::AutomaticallyManagedProjectFoldersInfoComponent)
|
||||
register(:general_information, ::Storages::Admin::GeneralInfoComponent)
|
||||
register(:nextcloud_audience, ::Storages::Admin::NextcloudAudienceInfoComponent)
|
||||
register(:oauth_application, ::Storages::Admin::OAuthApplicationInfoComponent)
|
||||
register(:oauth_client, ::Storages::Admin::OAuthClientInfoComponent)
|
||||
end
|
||||
|
||||
namespace("contracts") do
|
||||
register(:storage, ::Storages::Storages::NextcloudContract)
|
||||
register(:general_information, ::Storages::Storages::NextcloudGeneralInformationContract)
|
||||
register(:nextcloud_audience, ::Storages::Storages::NextcloudAudienceContract)
|
||||
end
|
||||
|
||||
namespace("models") do
|
||||
|
||||
@@ -33,13 +33,24 @@ module Storages
|
||||
class NextcloudStorageWizard < Wizard
|
||||
step :general_information, completed_if: ->(storage) { storage.host.present? && storage.name.present? }
|
||||
|
||||
# OAuth 2.0 SSO
|
||||
|
||||
step :nextcloud_audience,
|
||||
section: :oauth_configuration,
|
||||
if: ->(storage) { storage.authenticate_via_idp? },
|
||||
completed_if: ->(storage) { storage.nextcloud_audience.present? }
|
||||
|
||||
# Two-Way OAuth 2.0
|
||||
|
||||
step :oauth_application,
|
||||
section: :oauth_configuration,
|
||||
if: ->(storage) { !storage.authenticate_via_idp? },
|
||||
completed_if: ->(storage) { storage.oauth_application.present? },
|
||||
preparation: :prepare_oauth_application
|
||||
|
||||
step :oauth_client,
|
||||
section: :oauth_configuration,
|
||||
if: ->(storage) { !storage.authenticate_via_idp? },
|
||||
completed_if: ->(storage) { storage.oauth_client.present? },
|
||||
preparation: ->(storage) { storage.build_oauth_client }
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ module Storages
|
||||
|
||||
namespace("contracts") do
|
||||
register(:storage, ::Storages::Storages::OneDriveContract)
|
||||
register(:general_information, ::Storages::Storages::OneDriveContract)
|
||||
end
|
||||
|
||||
namespace("models") do
|
||||
|
||||
+6
@@ -31,6 +31,12 @@
|
||||
general_info_row.with_row(mb: 3) do
|
||||
render(Storages::Admin::ProviderHostInputForm.new(form))
|
||||
end
|
||||
|
||||
if OpenProject::FeatureDecisions.oidc_token_exchange_active?
|
||||
general_info_row.with_row(mb: 3) do
|
||||
render(Storages::Admin::NextcloudAuthenticationMethodInputForm.new(form))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if storage.provider_type_one_drive?
|
||||
|
||||
+14
-7
@@ -32,17 +32,16 @@ module Storages::Admin::Forms
|
||||
class GeneralInfoFormComponent < StorageFormComponent
|
||||
def self.wrapper_key = :storage_general_info_section
|
||||
|
||||
options form_method: :post,
|
||||
submit_button_disabled: false
|
||||
options submit_button_disabled: false
|
||||
|
||||
def form_url
|
||||
query = { continue_wizard: storage.id } if in_wizard
|
||||
query = { origin_component: "general_information" }
|
||||
query[:continue_wizard] = storage.id if in_wizard
|
||||
|
||||
case form_method
|
||||
when :get, :post
|
||||
admin_settings_storages_path(query)
|
||||
when :patch, :put
|
||||
if storage.persisted?
|
||||
admin_settings_storage_path(storage, query)
|
||||
else
|
||||
admin_settings_storages_path(query)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -62,6 +61,14 @@ module Storages::Admin::Forms
|
||||
|
||||
private
|
||||
|
||||
def form_method
|
||||
if storage.persisted?
|
||||
:patch
|
||||
else
|
||||
:post
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_button_path
|
||||
options.fetch(:cancel_button_path) do
|
||||
if storage.persisted?
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
component_wrapper(tag: "turbo-frame") do
|
||||
render(Primer::Beta::Text.new(tag: :div, test_selector: "storage-nextcloud-audience-form")) do
|
||||
primer_form_with(
|
||||
model:,
|
||||
url: form_url,
|
||||
method: :patch,
|
||||
data: { turbo_frame: "page-content" }
|
||||
) do |form|
|
||||
flex_layout do |general_info_row|
|
||||
general_info_row.with_row(mb: 3) do
|
||||
render(Storages::Admin::NextcloudAudienceInputForm.new(form))
|
||||
end
|
||||
|
||||
general_info_row.with_row do
|
||||
render(
|
||||
Storages::Admin::SubmitOrCancelForm.new(
|
||||
form,
|
||||
storage:,
|
||||
submit_button_options:,
|
||||
cancel_button_options:
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
%>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
#
|
||||
module Storages::Admin::Forms
|
||||
class NextcloudAudienceFormComponent < StorageFormComponent
|
||||
def self.wrapper_key = :storage_nextcloud_audience_section
|
||||
|
||||
options submit_button_disabled: false
|
||||
|
||||
def form_url
|
||||
query = { origin_component: "nextcloud_audience" }
|
||||
query[:continue_wizard] = storage.id if in_wizard
|
||||
|
||||
admin_settings_storage_path(storage, query)
|
||||
end
|
||||
|
||||
def submit_button_options
|
||||
{ disabled: submit_button_disabled }
|
||||
end
|
||||
|
||||
def cancel_button_options
|
||||
{ href: cancel_button_path, data: { turbo_stream: true } }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cancel_button_path
|
||||
edit_admin_settings_storage_path(storage)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
component_wrapper(tag: 'turbo-frame') do
|
||||
grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid|
|
||||
grid.with_area(:item, tag: :div, mr: 3) do
|
||||
concat(render(Primer::Beta::Text.new(font_weight: :bold, mr: 1, test_selector: 'nextcloud-audience-label')) { I18n.t('storages.file_storage_view.nextcloud_audience') })
|
||||
concat(configuration_check_label_for(:nextcloud_audience_configured))
|
||||
end
|
||||
|
||||
grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'nextcloud-audience-description') do
|
||||
concat(render(Primer::Beta::Text.new) {
|
||||
if storage.nextcloud_audience.present?
|
||||
I18n.t('storages.file_storage_view.nextcloud_audience_description', audience: storage.nextcloud_audience)
|
||||
else
|
||||
I18n.t('storages.file_storage_view.nextcloud_audience_blank')
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
if editable_storage?
|
||||
grid.with_area(:"icon-button", tag: :div, color: :muted) do
|
||||
flex_layout(justify_content: :flex_end) do |icons_container|
|
||||
icons_container.with_column do
|
||||
render(
|
||||
Primer::Beta::IconButton.new(
|
||||
icon: :pencil,
|
||||
tag: :a,
|
||||
scheme: :invisible,
|
||||
href: edit_nextcloud_audience_admin_settings_storage_path(storage),
|
||||
aria: { label: I18n.t('storages.label_edit_nextcloud_audience') },
|
||||
test_selector: 'storage-edit-nextcloud-audience-button',
|
||||
data: { turbo_stream: true }
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
%>
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages
|
||||
module Admin
|
||||
class NextcloudAudienceInfoComponent < StorageInfoComponent
|
||||
def self.wrapper_key = :storage_nextcloud_audience_section
|
||||
end
|
||||
end
|
||||
end
|
||||
+4
-4
@@ -56,8 +56,8 @@ module Storages::Admin
|
||||
end
|
||||
|
||||
def configuration_check_label_for(*configs)
|
||||
# do not show the status label, if storage is completely empty (initial state)
|
||||
return if storage.configuration_checks.values.none?
|
||||
# do not show the status label, if storage is in initial state
|
||||
return if storage.new_record?
|
||||
|
||||
if storage.configuration_checks.slice(*configs.map(&:to_sym)).values.all?
|
||||
status_label(I18n.t(:label_completed), scheme: :success, test_selector: "label-#{configs.join('-')}-status")
|
||||
@@ -71,8 +71,8 @@ module Storages::Admin
|
||||
end
|
||||
|
||||
def automatically_managed_project_folders_status_label
|
||||
# do not show the status label, if storage is completely empty (initial state)
|
||||
return if storage.configuration_checks.values.none?
|
||||
# do not show the status label, if storage is in initial state
|
||||
return if storage.new_record?
|
||||
|
||||
test_selector = "label-managed-project-folders-status"
|
||||
|
||||
@@ -39,8 +39,24 @@ module Storages::Storages
|
||||
class BaseContract < ::BaseContract
|
||||
include ::Storages::Storages::Concerns::ManageStoragesGuarded
|
||||
|
||||
attribute :name
|
||||
validates :name, presence: true, length: { maximum: 255 }
|
||||
class Factory
|
||||
def initialize(contract_class, provider_contract)
|
||||
@contract_class = contract_class
|
||||
@provider_contract = provider_contract
|
||||
end
|
||||
|
||||
def new(*, **)
|
||||
@contract_class.new(*, provider_contract: @provider_contract, **)
|
||||
end
|
||||
|
||||
delegate :<=, to: :@contract_class
|
||||
end
|
||||
|
||||
class << self
|
||||
def with_provider_contract(provider_contract)
|
||||
Factory.new(self, provider_contract)
|
||||
end
|
||||
end
|
||||
|
||||
attribute :provider_type
|
||||
validates :provider_type, inclusion: { in: Storages::Storage::PROVIDER_TYPES }, allow_nil: false
|
||||
@@ -50,11 +66,16 @@ module Storages::Storages
|
||||
validate :provider_type_strategy,
|
||||
unless: -> { errors.include?(:provider_type) || @options.delete(:skip_provider_type_strategy) }
|
||||
|
||||
def initialize(*, provider_contract: nil, **)
|
||||
super(*, **)
|
||||
|
||||
@provider_contract = provider_contract
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def provider_type_strategy
|
||||
contract = ::Storages::Peripherals::Registry.resolve("#{model.short_provider_type}.contracts.storage")
|
||||
.new(model, @user, options: @options)
|
||||
contract = provider_contract.new(model, @user, options: @options)
|
||||
|
||||
# Append the attributes defined in the internal contract
|
||||
# to the list of writable attributes.
|
||||
@@ -64,10 +85,12 @@ module Storages::Storages
|
||||
validate_and_merge_errors(contract)
|
||||
end
|
||||
|
||||
def require_ee_token_for_one_drive
|
||||
if ::Storages::Storage.one_drive_without_ee_token?(provider_type)
|
||||
errors.add(:base, I18n.t("api_v3.errors.code_500_missing_enterprise_token"))
|
||||
end
|
||||
def provider_contract
|
||||
@provider_contract || default_provider_contract
|
||||
end
|
||||
|
||||
def default_provider_contract
|
||||
::Storages::Peripherals::Registry.resolve("#{model.short_provider_type}.contracts.storage")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,5 +41,11 @@ module Storages::Storages
|
||||
errors.add(:creator, :invalid)
|
||||
end
|
||||
end
|
||||
|
||||
def require_ee_token_for_one_drive
|
||||
if ::Storages::Storage.one_drive_without_ee_token?(provider_type)
|
||||
errors.add(:base, I18n.t("api_v3.errors.code_500_missing_enterprise_token"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Storages
|
||||
class NextcloudAudienceContract < ::ModelContract
|
||||
attribute :nextcloud_audience
|
||||
validates :nextcloud_audience, presence: true, if: -> { nextcloud_storage_authenticate_via_idp? }
|
||||
|
||||
# Adding this to allow writing the nextcloud_audience
|
||||
attribute :provider_fields
|
||||
|
||||
private
|
||||
|
||||
def nextcloud_storage_authenticate_via_idp?
|
||||
nextcloud_storage? && @model.authenticate_via_idp?
|
||||
end
|
||||
|
||||
def nextcloud_storage?
|
||||
@model.is_a?(Storages::NextcloudStorage)
|
||||
end
|
||||
end
|
||||
end
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Storages
|
||||
class NextcloudAutomaticManagementContract < ::ModelContract
|
||||
attribute :automatically_managed
|
||||
|
||||
attribute :username
|
||||
validates :username, presence: true, if: :nextcloud_storage_automatic_management_enabled?
|
||||
validates :username,
|
||||
absence: true,
|
||||
unless: -> { nextcloud_storage_automatic_management_enabled? || nextcloud_default_storage_username? }
|
||||
|
||||
attribute :password
|
||||
validates :password, presence: true, if: :nextcloud_storage_automatic_management_enabled?
|
||||
validates :password, absence: true, unless: :nextcloud_storage_automatic_management_enabled?
|
||||
|
||||
validate do
|
||||
if nextcloud_storage_automatic_management_enabled? && errors.exclude?(:password) && model.host.present?
|
||||
NextcloudApplicationCredentialsValidator.new(self).call
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def nextcloud_storage_automatic_management_enabled?
|
||||
return false unless nextcloud_storage?
|
||||
|
||||
@model.automatic_management_enabled?
|
||||
end
|
||||
|
||||
def nextcloud_default_storage_username?
|
||||
return false unless nextcloud_storage?
|
||||
|
||||
@model.username == @model.provider_fields_defaults[:username]
|
||||
end
|
||||
|
||||
def nextcloud_storage?
|
||||
@model.is_a?(Storages::NextcloudStorage)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,57 +29,9 @@
|
||||
#++
|
||||
|
||||
module Storages::Storages
|
||||
class NextcloudContract < ::ModelContract
|
||||
attribute :host
|
||||
validates :host, url: { message: :invalid_host_url }, length: { maximum: 255 }
|
||||
# Check that a host actually is a storage server.
|
||||
# But only do so if the validations above for URL were successful.
|
||||
validates :host, secure_context_uri: true, nextcloud_compatible_host: true, unless: -> { errors.include?(:host) }
|
||||
|
||||
attribute :authentication_method
|
||||
validates :authentication_method, presence: true, inclusion: { in: ::Storages::NextcloudStorage::AUTHENTICATION_METHODS }
|
||||
|
||||
attribute :nextcloud_audience
|
||||
validates :nextcloud_audience, presence: true, if: :nextcloud_storage_authenticate_via_idp?
|
||||
|
||||
attribute :automatically_managed
|
||||
|
||||
attribute :username
|
||||
validates :username, presence: true, if: :nextcloud_storage_automatic_management_enabled?
|
||||
validates :username,
|
||||
absence: true,
|
||||
unless: -> { nextcloud_storage_automatic_management_enabled? || nextcloud_default_storage_username? }
|
||||
|
||||
attribute :password
|
||||
validates :password, presence: true, if: :nextcloud_storage_automatic_management_enabled?
|
||||
validates :password, absence: true, unless: :nextcloud_storage_automatic_management_enabled?
|
||||
|
||||
validate do
|
||||
if nextcloud_storage_automatic_management_enabled? && errors.exclude?(:host) && errors.exclude?(:password)
|
||||
NextcloudApplicationCredentialsValidator.new(self).call
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def nextcloud_storage_automatic_management_enabled?
|
||||
return false unless nextcloud_storage?
|
||||
|
||||
@model.automatic_management_enabled?
|
||||
end
|
||||
|
||||
def nextcloud_default_storage_username?
|
||||
return false unless nextcloud_storage?
|
||||
|
||||
@model.username == @model.provider_fields_defaults[:username]
|
||||
end
|
||||
|
||||
def nextcloud_storage_authenticate_via_idp?
|
||||
nextcloud_storage? && @model.authenticate_via_idp?
|
||||
end
|
||||
|
||||
def nextcloud_storage?
|
||||
@model.is_a?(Storages::NextcloudStorage)
|
||||
end
|
||||
class NextcloudContract < ComposedContract
|
||||
include_contract NextcloudGeneralInformationContract
|
||||
include_contract NextcloudAudienceContract
|
||||
include_contract NextcloudAutomaticManagementContract
|
||||
end
|
||||
end
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Storages
|
||||
class NextcloudGeneralInformationContract < ::ModelContract
|
||||
attribute :name
|
||||
validates :name, presence: true, length: { maximum: 255 }
|
||||
attribute :host
|
||||
validates :host, url: { message: :invalid_host_url }, length: { maximum: 255 }
|
||||
# Check that a host actually is a storage server.
|
||||
# But only do so if the validations above for URL were successful.
|
||||
validates :host, secure_context_uri: true, nextcloud_compatible_host: true, unless: -> { errors.include?(:host) }
|
||||
|
||||
attribute :authentication_method
|
||||
validates :authentication_method, presence: true, inclusion: { in: ::Storages::NextcloudStorage::AUTHENTICATION_METHODS }
|
||||
end
|
||||
end
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
module Storages::Storages
|
||||
class OneDriveContract < ::ModelContract
|
||||
attribute :name
|
||||
validates :name, presence: true, length: { maximum: 255 }
|
||||
attribute :host
|
||||
validates :host, absence: true
|
||||
attribute :tenant_id
|
||||
|
||||
@@ -45,7 +45,7 @@ class Storages::Admin::StoragesController < ApplicationController
|
||||
# and set the @<controller_name> variable to the object referenced in the URL.
|
||||
before_action :require_admin
|
||||
before_action :find_model_object,
|
||||
only: %i[show_oauth_application destroy edit edit_host confirm_destroy update
|
||||
only: %i[show_oauth_application destroy edit edit_host edit_nextcloud_audience confirm_destroy update
|
||||
change_health_notifications_enabled replace_oauth_application]
|
||||
before_action :ensure_valid_wizard_parameters, only: [:new]
|
||||
before_action :require_ee_token_for_one_drive, only: [:new]
|
||||
@@ -83,8 +83,13 @@ class Storages::Admin::StoragesController < ApplicationController
|
||||
|
||||
def create
|
||||
service_result = Storages::Storages::CreateService
|
||||
.new(user: current_user, create_oauth_app: false)
|
||||
.call(permitted_storage_params)
|
||||
.new(
|
||||
user: current_user,
|
||||
create_oauth_app: false,
|
||||
contract_class: Storages::Storages::CreateContract.with_provider_contract(
|
||||
current_step_contract(permitted_storage_params[:provider_type])
|
||||
)
|
||||
).call(permitted_storage_params)
|
||||
|
||||
@storage = service_result.result
|
||||
|
||||
@@ -112,21 +117,25 @@ class Storages::Admin::StoragesController < ApplicationController
|
||||
|
||||
def edit_host
|
||||
update_via_turbo_stream(
|
||||
component: Storages::Admin::Forms::GeneralInfoFormComponent.new(
|
||||
@storage,
|
||||
form_method: :patch,
|
||||
cancel_button_path: edit_admin_settings_storage_path(@storage)
|
||||
)
|
||||
component: Storages::Admin::Forms::GeneralInfoFormComponent.new(@storage)
|
||||
)
|
||||
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def edit_nextcloud_audience
|
||||
update_via_turbo_stream(component: Storages::Admin::Forms::NextcloudAudienceFormComponent.new(@storage))
|
||||
respond_with_turbo_streams
|
||||
end
|
||||
|
||||
def update # rubocop:disable Metrics/AbcSize
|
||||
contract_class = Storages::Storages::UpdateContract.with_provider_contract(current_step_contract(@storage))
|
||||
service_result = ::Storages::Storages::UpdateService
|
||||
.new(user: current_user, model: @storage)
|
||||
.call(permitted_storage_params)
|
||||
@storage = service_result.result
|
||||
.new(
|
||||
user: current_user,
|
||||
model: @storage,
|
||||
contract_class:
|
||||
).call(permitted_storage_params)
|
||||
|
||||
if service_result.success?
|
||||
if params[:continue_wizard]
|
||||
@@ -135,11 +144,10 @@ class Storages::Admin::StoragesController < ApplicationController
|
||||
redirect_to(edit_admin_settings_storage_path(@storage), status: :see_other)
|
||||
end
|
||||
else
|
||||
origin_component = params[:origin_component].presence || "general_information"
|
||||
update_via_turbo_stream(
|
||||
component: Storages::Admin::Forms::GeneralInfoFormComponent.new(
|
||||
component: ::Storages::Peripherals::Registry.resolve("#{@storage}.components.forms.#{origin_component}").new(
|
||||
@storage,
|
||||
form_method: :patch,
|
||||
cancel_button_path: edit_admin_settings_storage_path(@storage),
|
||||
in_wizard: params[:continue_wizard].present?
|
||||
)
|
||||
)
|
||||
@@ -243,6 +251,8 @@ class Storages::Admin::StoragesController < ApplicationController
|
||||
.permit("name",
|
||||
"provider_type",
|
||||
"host",
|
||||
"authentication_method",
|
||||
"nextcloud_audience",
|
||||
"oauth_client_id",
|
||||
"oauth_client_secret",
|
||||
"tenant_id",
|
||||
@@ -275,4 +285,11 @@ class Storages::Admin::StoragesController < ApplicationController
|
||||
def redirect_to_wizard(storage)
|
||||
redirect_to(new_admin_settings_storage_path(continue_wizard: storage.id), status: :see_other)
|
||||
end
|
||||
|
||||
def current_step_contract(storage)
|
||||
storage_name = storage.is_a?(String) ? ::Storages::Storage.shorten_provider_type(storage) : storage.to_s
|
||||
origin_component = params[:origin_component].presence || "general_information"
|
||||
|
||||
::Storages::Peripherals::Registry.resolve("#{storage_name}.contracts.#{origin_component}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Admin
|
||||
class NextcloudAudienceInputForm < ApplicationForm
|
||||
form do |storage_form|
|
||||
storage_form.text_field(
|
||||
name: :nextcloud_audience,
|
||||
label: I18n.t("activerecord.attributes.storages/nextcloud_storage.nextcloud_audience"),
|
||||
required: true,
|
||||
caption: I18n.t("storages.instructions.nextcloud.nextcloud_audience"),
|
||||
placeholder: I18n.t("storages.instructions.nextcloud.nextcloud_audience_placeholder"),
|
||||
input_width: :large
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Storages::Admin
|
||||
class NextcloudAuthenticationMethodInputForm < ApplicationForm
|
||||
form do |storage_form|
|
||||
storage_form.select_list(
|
||||
name: :authentication_method,
|
||||
label: I18n.t("activerecord.attributes.storages/storage.authentication_method"),
|
||||
visually_hide_label: false,
|
||||
required: true,
|
||||
caption: I18n.t("storages.instructions.authentication_method"),
|
||||
input_width: :large
|
||||
) do |list|
|
||||
::Storages::NextcloudStorage::AUTHENTICATION_METHODS.each do |m|
|
||||
list.option(label: I18n.t("activerecord.attributes.storages/nextcloud_storage.authentication_methods.#{m}"), value: m)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -78,7 +78,8 @@ module Storages
|
||||
{
|
||||
storage_oauth_client_configured: oauth_client.present?,
|
||||
openproject_oauth_application_configured: oauth_application.present?,
|
||||
host_name_configured: host.present? && name.present?
|
||||
host_name_configured: host.present? && name.present?,
|
||||
nextcloud_audience_configured: !authenticate_via_idp? || nextcloud_audience.present?
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ module Storages::Storages
|
||||
storage = service_call.result
|
||||
# Automatically create an OAuthApplication object for the Nextcloud storage
|
||||
# using values from storage (particularly :host) as defaults
|
||||
if storage.provider_type_nextcloud?
|
||||
if storage.provider_type_nextcloud? && !storage.authenticate_via_idp?
|
||||
persist_service_result = ::Storages::OAuthApplications::CreateService.new(storage:, user:).call
|
||||
storage.oauth_application = persist_service_result.result if persist_service_result.success?
|
||||
service_call.add_dependent!(persist_service_result)
|
||||
|
||||
@@ -4,9 +4,15 @@ en:
|
||||
attributes:
|
||||
storages/file_link:
|
||||
origin_id: Origin Id
|
||||
storages/nextcloud_storage:
|
||||
authentication_methods:
|
||||
oauth2_sso: Single-Sign-On through OpenID Connect Identity Provider
|
||||
two_way_oauth2: Two-way OAuth 2.0 authorization code flow
|
||||
nextcloud_audience: Nextcloud Audience
|
||||
storages/project_storage:
|
||||
storage: Storage
|
||||
storages/storage:
|
||||
authentication_method: Authentication Method
|
||||
creator: Creator
|
||||
drive: Drive ID
|
||||
host: Host
|
||||
@@ -203,6 +209,9 @@ en:
|
||||
access_management_section: Access and project folders
|
||||
automatically_managed_folders: Automatically managed folders
|
||||
general_information: General information
|
||||
nextcloud_audience: Nextcloud Audience
|
||||
nextcloud_audience_blank: No audience has been configured
|
||||
nextcloud_audience_description: Using audience %{audience}
|
||||
nextcloud_oauth: Nextcloud OAuth
|
||||
oauth_configuration: OAuth configuration
|
||||
one_drive_oauth: Azure OAuth
|
||||
@@ -254,6 +263,7 @@ en:
|
||||
project_folder_bulk: The project folder is the default folder for file uploads for all the projects selected. You can modify this individually in each project settings. Users can nevertheless still upload files to other locations.
|
||||
instructions:
|
||||
all_available_storages_already_added: All available storages are already added to the project.
|
||||
authentication_method: The way that requests between OpenProject and the Storage are authenticated.
|
||||
automatic_folder: This will automatically create a root folder for this project and manage the access permissions for each project member.
|
||||
empty_project_folder_validation: Selecting a folder is mandatory to proceed.
|
||||
existing_manual_folder: You can designate an existing folder as the root folder for this project. The permissions are however not automatically managed, the administrator needs to manually ensure relevant users have access. The selected folder can be used by multiple projects.
|
||||
@@ -264,6 +274,8 @@ en:
|
||||
nextcloud:
|
||||
application_link_text: application “Integration OpenProject”
|
||||
integration: Nextcloud Administration / OpenProject
|
||||
nextcloud_audience: Please enter the Client ID that the Nextcloud instance uses to communicate with the identity provider.
|
||||
nextcloud_audience_placeholder: e.g. nextcloud
|
||||
oauth_configuration: Copy these values from %{application_link_text}.
|
||||
provider_configuration: Please make sure you have administration privileges in your Nextcloud instance and the %{application_link_text} is installed before doing the setup.
|
||||
no_specific_folder: By default, each user will start at their own home folder when they upload a file.
|
||||
@@ -296,6 +308,7 @@ en:
|
||||
label_creation_time: Creation time
|
||||
label_creator: Creator
|
||||
label_delete_storage: Delete storage
|
||||
label_edit_nextcloud_audience: Edit Nextcloud Audience
|
||||
label_edit_storage_access_management: Edit storage access management
|
||||
label_edit_storage_automatically_managed_folders: Edit storage automatically managed folders
|
||||
label_edit_storage_host: Edit storage host
|
||||
|
||||
@@ -62,6 +62,7 @@ Rails.application.routes.draw do
|
||||
member do
|
||||
get :show_oauth_application
|
||||
get :edit_host
|
||||
get :edit_nextcloud_audience
|
||||
patch :change_health_notifications_enabled
|
||||
get :confirm_destroy
|
||||
delete :replace_oauth_application
|
||||
|
||||
@@ -51,10 +51,11 @@ RSpec.describe Storages::Peripherals::NextcloudStorageWizard do
|
||||
automatically_managed_folders])
|
||||
end
|
||||
|
||||
context "when name and host were set" do
|
||||
context "when name and host were set and authentication method is Two-Way OAuth 2.0" do
|
||||
before do
|
||||
model.name = "Karl"
|
||||
model.host = "https://nextcloud.local/"
|
||||
model.authentication_method = "two_way_oauth2"
|
||||
model.save!
|
||||
end
|
||||
|
||||
@@ -131,4 +132,61 @@ RSpec.describe Storages::Peripherals::NextcloudStorageWizard do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when name and host were set and authentication method is OAuth 2.0 SSO" do
|
||||
before do
|
||||
model.name = "Karl"
|
||||
model.host = "https://nextcloud.local/"
|
||||
model.authentication_method = "oauth2_sso"
|
||||
model.save!
|
||||
end
|
||||
|
||||
it "has general_information step completed" do
|
||||
expect(wizard.completed_steps).to eq(%i[general_information])
|
||||
end
|
||||
|
||||
it "has new steps pending in correct order" do
|
||||
expect(wizard.pending_steps).to eq(%i[nextcloud_audience
|
||||
automatically_managed_folders])
|
||||
end
|
||||
|
||||
context "and the nextcloud audience was set" do
|
||||
before do
|
||||
model.nextcloud_audience = "nextcloud"
|
||||
end
|
||||
|
||||
it "finished the nextcloud_audience step" do
|
||||
expect(wizard.completed_steps).to eq(%i[general_information
|
||||
nextcloud_audience])
|
||||
end
|
||||
|
||||
it "still didn't specify how to manage folders" do
|
||||
expect(model).to be_automatic_management_unspecified
|
||||
end
|
||||
|
||||
context "and after preparing the next step" do
|
||||
before do
|
||||
wizard.prepare_next_step
|
||||
end
|
||||
|
||||
it "enabled automatic storage management, but didn't persist it" do
|
||||
expect(model).to be_automatic_management_enabled
|
||||
|
||||
before, after = model.changes["provider_fields"]
|
||||
expect(before.keys).not_to include("automatically_managed")
|
||||
expect(after.keys).to include("automatically_managed")
|
||||
end
|
||||
|
||||
it "has no pending steps" do
|
||||
expect(wizard.pending_steps).to be_empty
|
||||
end
|
||||
|
||||
it "has all steps completed" do
|
||||
expect(wizard.completed_steps).to eq(%i[general_information
|
||||
nextcloud_audience
|
||||
automatically_managed_folders])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -343,4 +343,10 @@ RSpec.shared_examples_for "nextcloud storage contract", :storage_server_helpers,
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".with_provider_contract" do
|
||||
it "looks like it descends from Storages::Storages::BaseContract" do
|
||||
expect(described_class.with_provider_contract(instance_double(Class))).to be <= Storages::Storages::BaseContract
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,7 +46,7 @@ RSpec.describe "Admin Create a new file storage",
|
||||
allow(Doorkeeper::OAuth::Helpers::UniqueToken).to receive(:generate).and_return(secret)
|
||||
end
|
||||
|
||||
it "renders a Nextcloud specific multi-step form", :webmock do
|
||||
it "renders a Nextcloud specific multi-step form for Two-Way OAuth 2.0 by default", :webmock do
|
||||
visit admin_settings_storages_path
|
||||
|
||||
expect(page).to be_axe_clean.within "#content"
|
||||
@@ -58,7 +58,7 @@ RSpec.describe "Admin Create a new file storage",
|
||||
|
||||
expect(page).to have_current_path(new_admin_settings_storage_path(provider: "nextcloud"))
|
||||
|
||||
aggregate_failures "Select provider view" do
|
||||
aggregate_failures "New provider view" do
|
||||
# Page Header
|
||||
expect(page).to have_test_selector("storage-new-page-header--title", text: "New Nextcloud storage")
|
||||
expect(page).to have_test_selector("storage-new-page-header--description",
|
||||
@@ -188,6 +188,81 @@ RSpec.describe "Admin Create a new file storage",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "renders a Nextcloud specific multi-step form when using OAuth 2.0 SSO", :webmock, with_flag: :oidc_token_exchange do
|
||||
# Same setup as in default case, but without expectations
|
||||
visit admin_settings_storages_path
|
||||
|
||||
within(".SubHeader") do
|
||||
page.find_test_selector("storages-create-new-provider-button").click
|
||||
within_test_selector("storages-select-provider-action-menu") { click_on("Nextcloud") }
|
||||
end
|
||||
|
||||
within_test_selector("storage-general-info-form") do
|
||||
fill_in "Name", with: "My Nextcloud", fill_options: { clear: :backspace }
|
||||
|
||||
mock_server_capabilities_response("https://example.com")
|
||||
mock_server_config_check_response("https://example.com")
|
||||
fill_in "Host", with: "https://example.com"
|
||||
|
||||
select "Single-Sign-On through OpenID Connect Identity Provider", from: "Authentication Method"
|
||||
|
||||
click_on "Save and continue"
|
||||
end
|
||||
|
||||
aggregate_failures "Nextcloud Audience" do
|
||||
within_test_selector("storage-nextcloud-audience-form") do
|
||||
click_on "Save and continue"
|
||||
expect(page).to have_text("Nextcloud Audience can't be blank.")
|
||||
|
||||
fill_in "Nextcloud Audience", with: "nextcloud"
|
||||
click_on "Save and continue"
|
||||
end
|
||||
|
||||
expect(page).to have_test_selector("label-nextcloud_audience_configured-status", text: "Completed")
|
||||
expect(page).to have_test_selector("nextcloud-audience-description", text: "Using audience nextcloud")
|
||||
end
|
||||
|
||||
aggregate_failures "Automatically managed project folders" do
|
||||
within_test_selector("storage-automatically-managed-project-folders-form") do
|
||||
automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatic_management_enabled]"]')
|
||||
application_password_input = page.find_by_id("storages_nextcloud_storage_password")
|
||||
expect(automatically_managed_switch).to be_checked
|
||||
expect(application_password_input.value).to be_empty
|
||||
|
||||
# Clicking submit with application password empty should show an error
|
||||
click_on("Done, complete setup")
|
||||
expect(page).to have_text("Password can't be blank.")
|
||||
|
||||
# Test the error path for an invalid storage password.
|
||||
# Mock a valid response (=401) for example.com, so the password validation should fail
|
||||
mock_nextcloud_application_credentials_validation("https://example.com", password: "1234567890",
|
||||
response_code: 401)
|
||||
automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatic_management_enabled]"]')
|
||||
expect(automatically_managed_switch).to be_checked
|
||||
fill_in "Application password", with: "1234567890"
|
||||
# Clicking submit with application password empty should show an error
|
||||
click_on("Done, complete setup")
|
||||
expect(page).to have_text("Password is not valid.")
|
||||
|
||||
# Test the happy path for a valid storage password.
|
||||
# Mock a valid response (=200) for example.com, so the password validation should succeed
|
||||
# Fill in application password and submit
|
||||
mock_nextcloud_application_credentials_validation("https://example.com", password: "1234567890")
|
||||
automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatic_management_enabled]"]')
|
||||
expect(automatically_managed_switch).to be_checked
|
||||
fill_in "Application password", with: "1234567890"
|
||||
click_on("Done, complete setup")
|
||||
end
|
||||
|
||||
expect(page).to have_current_path(edit_admin_settings_storage_path(Storages::Storage.last))
|
||||
expect(page).to have_test_selector(
|
||||
"op-primer-flash-message",
|
||||
text: "Storage connected successfully! " \
|
||||
"Remember to activate the storage in the Projects tab for each desired project to use it."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with OneDrive Storage and enterprise token missing", with_ee: false do
|
||||
@@ -221,7 +296,7 @@ RSpec.describe "Admin Create a new file storage",
|
||||
|
||||
expect(page).to have_current_path(new_admin_settings_storage_path(provider: "one_drive"))
|
||||
|
||||
aggregate_failures "Select provider view" do
|
||||
aggregate_failures "New provider view" do
|
||||
# Page Header
|
||||
expect(page).to have_test_selector("storage-new-page-header--title", text: "New OneDrive/SharePoint storage")
|
||||
expect(page).to have_test_selector("storage-new-page-header--description",
|
||||
|
||||
@@ -59,7 +59,7 @@ RSpec.describe "Admin Edit File storage",
|
||||
expect(page).to have_current_path(admin_settings_storages_path)
|
||||
end
|
||||
|
||||
context "with Nextcloud Storage" do
|
||||
context "with Two-Way OAuth Nextcloud Storage" do
|
||||
let(:storage) { create(:nextcloud_storage, :as_automatically_managed, name: "Cloud Storage") }
|
||||
let(:oauth_application) { create(:oauth_application, integration: storage) }
|
||||
let(:oauth_client) { create(:oauth_client, integration: storage) }
|
||||
@@ -241,6 +241,94 @@ RSpec.describe "Admin Edit File storage",
|
||||
end
|
||||
end
|
||||
|
||||
context "with OAuth 2.0 SSO Nextcloud Storage" do
|
||||
let(:storage) do
|
||||
create(
|
||||
:nextcloud_storage,
|
||||
:as_automatically_managed,
|
||||
authentication_method: "oauth2_sso",
|
||||
nextcloud_audience: "",
|
||||
name: "Cloud Storage"
|
||||
)
|
||||
end
|
||||
let(:secret) { "awesome_secret" }
|
||||
|
||||
before do
|
||||
allow(Doorkeeper::OAuth::Helpers::UniqueToken).to receive(:generate).and_return(secret)
|
||||
end
|
||||
|
||||
it "renders an edit view", :webmock do
|
||||
visit edit_admin_settings_storage_path(storage)
|
||||
|
||||
expect(page).to be_axe_clean
|
||||
.within("#content")
|
||||
# NB: Heading order is pending app wide update. See https://community.openproject.org/projects/openproject/work_packages/48513
|
||||
.skipping("heading-order")
|
||||
|
||||
expect(page).to have_test_selector("storage-new-page-header--title", text: "Cloud Storage (Nextcloud)")
|
||||
|
||||
aggregate_failures "Storage edit view" do
|
||||
# General information
|
||||
expect(page).to have_test_selector("storage-provider-label", text: "Storage provider")
|
||||
expect(page).to have_test_selector("label-host_name_configured-status", text: "Completed")
|
||||
expect(page).to have_test_selector("storage-description", text: "Nextcloud - #{storage.name} - #{storage.host}")
|
||||
|
||||
# Nextcloud audience
|
||||
expect(page).to have_test_selector("nextcloud-audience-label", text: "Nextcloud Audience")
|
||||
expect(page).to have_test_selector("label-nextcloud_audience_configured-status", text: "Incomplete")
|
||||
expect(page).to have_test_selector("nextcloud-audience-description", text: "No audience has been configured")
|
||||
|
||||
# Automatically managed project folders
|
||||
expect(page).to have_test_selector("storage-managed-project-folders-label",
|
||||
text: "Automatically managed folders")
|
||||
|
||||
expect(page).to have_test_selector("label-managed-project-folders-status", text: "Active")
|
||||
expect(page).to have_test_selector("storage-automatically-managed-project-folders-description",
|
||||
text: "Let OpenProject create folders per project automatically.")
|
||||
end
|
||||
|
||||
# Only testing interaction with components not tested
|
||||
# in Two-Way OAuth 2.0 case
|
||||
|
||||
aggregate_failures "Nextcloud Audience" do
|
||||
find_test_selector("storage-edit-nextcloud-audience-button").click
|
||||
within_test_selector("storage-nextcloud-audience-form") do
|
||||
click_on "Save and continue"
|
||||
expect(page).to have_text("Nextcloud Audience can't be blank")
|
||||
|
||||
fill_in "Nextcloud Audience", with: "schmaudience"
|
||||
click_on "Save and continue"
|
||||
end
|
||||
|
||||
expect(page).to have_test_selector("label-nextcloud_audience_configured-status", text: "Completed")
|
||||
expect(page).to have_test_selector("nextcloud-audience-description", text: "Using audience schmaudience")
|
||||
end
|
||||
end
|
||||
|
||||
it "renders a sidebar component" do
|
||||
visit edit_admin_settings_storage_path(storage)
|
||||
|
||||
aggregate_failures "Health status" do
|
||||
expect(page).to have_test_selector("validation-result--subtitle", text: "Connection validation")
|
||||
expect(page).to have_test_selector("storage-health-status", text: "Pending")
|
||||
end
|
||||
|
||||
aggregate_failures "Health notifications" do
|
||||
expect(page).to have_test_selector("storage-health-notifications-button", text: "Unsubscribe")
|
||||
expect(page).to have_test_selector("storage-health-notifications-description",
|
||||
text: "All administrators receive health status email notifications for this storage.")
|
||||
|
||||
click_on "Unsubscribe"
|
||||
|
||||
expect(page).to have_test_selector("storage-health-notifications-button", text: "Subscribe")
|
||||
expect(page).to have_test_selector(
|
||||
"storage-health-notifications-description",
|
||||
text: "Health status email notifications for this storage have been turned off for all administrators."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with Nextcloud Storage and not automatically managed" do
|
||||
let(:storage) { create(:nextcloud_storage, :as_not_automatically_managed, name: "Cloud Storage") }
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ RSpec.describe Storages::NextcloudStorage do
|
||||
aggregate_failures "configuration_checks" do
|
||||
expect(storage.configuration_checks)
|
||||
.to eq(host_name_configured: true,
|
||||
nextcloud_audience_configured: true,
|
||||
storage_oauth_client_configured: true,
|
||||
openproject_oauth_application_configured: true)
|
||||
end
|
||||
@@ -84,6 +85,43 @@ RSpec.describe Storages::NextcloudStorage do
|
||||
end
|
||||
end
|
||||
|
||||
context "without nextcloud audience" do
|
||||
let(:storage) do
|
||||
build(:nextcloud_storage,
|
||||
nextcloud_audience: "",
|
||||
oauth_application: build(:oauth_application),
|
||||
oauth_client: build(:oauth_client))
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
aggregate_failures do
|
||||
expect(storage.configured?).to be(true)
|
||||
aggregate_failures "configuration_checks" do
|
||||
expect(storage.configuration_checks[:nextcloud_audience_configured]).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when storage authenticates through IDP" do
|
||||
let(:storage) do
|
||||
build(:nextcloud_storage,
|
||||
authentication_method: "oauth2_sso",
|
||||
nextcloud_audience: "",
|
||||
oauth_application: build(:oauth_application),
|
||||
oauth_client: build(:oauth_client))
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
aggregate_failures do
|
||||
expect(storage.configured?).to be(false)
|
||||
aggregate_failures "configuration_checks" do
|
||||
expect(storage.configuration_checks[:nextcloud_audience_configured]).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without openproject and storage integrations" do
|
||||
let(:storage) { build(:nextcloud_storage) }
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ require "services/base_services/behaves_like_create_service"
|
||||
|
||||
RSpec.describe Storages::Storages::CreateService, type: :model do
|
||||
it_behaves_like "BaseServices create service" do
|
||||
let(:factory) { :storage }
|
||||
let(:factory) { :nextcloud_storage }
|
||||
|
||||
let!(:user) { create(:admin) }
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe ComposedContract do
|
||||
subject(:composed_contract) do
|
||||
# Binding methods to local variables so they are accessible inside the newly defined class
|
||||
a = contract_class_a
|
||||
b = contract_class_b
|
||||
Class.new(described_class) do
|
||||
include_contract a
|
||||
include_contract b
|
||||
end.new(model, user)
|
||||
end
|
||||
|
||||
let(:contract_class_a) do
|
||||
Class.new(ModelContract) do
|
||||
attribute :subject
|
||||
validates :subject, format: /B/
|
||||
end
|
||||
end
|
||||
let(:contract_class_b) do
|
||||
Class.new(ModelContract) do
|
||||
attribute :description
|
||||
validates :description, format: /E/
|
||||
end
|
||||
end
|
||||
|
||||
let(:model) do
|
||||
create :work_package
|
||||
end
|
||||
let(:title) { "ABC" }
|
||||
let(:description) { "DEF" }
|
||||
|
||||
let(:user) { build(:user) }
|
||||
|
||||
before do
|
||||
model.save!
|
||||
model.assign_attributes(subject: title, description:)
|
||||
end
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
|
||||
context "when contract A is invalid" do
|
||||
let(:title) { "XYZ" }
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
context "when contract B is invalid" do
|
||||
let(:description) { "XYZ" }
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
context "when changing an attribute not allowed by any subcontract" do
|
||||
before do
|
||||
model.done_ratio = 50
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
|
||||
context "and when the attribute was allowed on the composed contract" do
|
||||
before do
|
||||
composed_contract.writable_attributes << "done_ratio"
|
||||
end
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user