diff --git a/Gemfile b/Gemfile index a6990144e10..4eaf9b4be08 100644 --- a/Gemfile +++ b/Gemfile @@ -211,7 +211,6 @@ gem "mini_magick", "~> 5.2.0", require: false gem "validate_url" # Storages support code -gem "dry-auto_inject" gem "dry-container" gem "dry-monads" gem "dry-validation" diff --git a/Gemfile.lock b/Gemfile.lock index 2dd07bbd3e0..5d3b2960a3a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -481,9 +481,6 @@ GEM dotenv (= 3.1.8) railties (>= 6.1) drb (2.2.3) - dry-auto_inject (1.1.0) - dry-core (~> 1.1) - zeitwerk (~> 2.6) dry-configurable (1.3.0) dry-core (~> 1.1) zeitwerk (~> 2.6) @@ -1382,7 +1379,6 @@ DEPENDENCIES disposable (~> 0.6.2) doorkeeper (~> 5.8.0) dotenv-rails - dry-auto_inject dry-container dry-monads dry-validation @@ -1655,7 +1651,6 @@ CHECKSUMS dotenv (3.1.8) sha256=9e1176060ced581f8e6ce4384e91361817763a76e3c625c8bddc18b35bd392c3 dotenv-rails (3.1.8) sha256=46c9d1226a8b58a83b5f61325aa8cffd25cea1c0fafdfbbbee1e5dfea77980c4 drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 - dry-auto_inject (1.1.0) sha256=f9276cb5d15a3ef138e1f1149e289e287f636de57ef4a6decd233542eb708f78 dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8 dry-container (0.11.0) sha256=23be9381644d47343f3bf13b082b4255994ada0bfd88e0737eaaadc99d035229 dry-core (1.1.0) sha256=0903821a9707649a7da545a2cd88e20f3a663ab1c5288abd7f914fa7751ab195 diff --git a/app/controllers/oauth_clients_controller.rb b/app/controllers/oauth_clients_controller.rb index 365fefee171..40da684c61b 100644 --- a/app/controllers/oauth_clients_controller.rb +++ b/app/controllers/oauth_clients_controller.rb @@ -82,8 +82,7 @@ class OAuthClientsController < ApplicationController storage = oauth_client.integration # check if the origin is the same destination_url = destination_url(params.fetch(:destination_url, "")) - auth_state = ::Storages::Peripherals::StorageInteraction::Authentication - .authorization_state(storage:, user: User.current) + auth_state = ::Storages::Adapters::Authentication.authorization_state(storage:, user: User.current) if auth_state == :connected redirect_to(destination_url) diff --git a/app/models/remote_identity.rb b/app/models/remote_identity.rb index 2c4a8d6e62e..932127e40cd 100644 --- a/app/models/remote_identity.rb +++ b/app/models/remote_identity.rb @@ -35,4 +35,7 @@ class RemoteIdentity < ApplicationRecord validates :user, uniqueness: { scope: %i[auth_source integration] } validates :origin_user_id, :user, :auth_source, :integration, presence: true + + # FIXME: This needs a better name - 2025.03.18 @mereghost + scope :of_user_and_client, ->(user, client, integration) { find_by(user:, auth_source: client, integration:) } end diff --git a/app/models/user.rb b/app/models/user.rb index f3595c16f78..be882e841a8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -317,6 +317,10 @@ class User < Principal authentication_provider&.display_name end + def provided_by_oidc? + authentication_provider.is_a?(OpenIDConnect::Provider) + end + ## # Allows the API and other sources to determine locking actions # on represented collections of children of Principals. diff --git a/app/services/remote_identities/create_service.rb b/app/services/remote_identities/create_service.rb index 59373bb2a8d..34626f03e3e 100644 --- a/app/services/remote_identities/create_service.rb +++ b/app/services/remote_identities/create_service.rb @@ -47,10 +47,11 @@ module RemoteIdentities def call if @model.new_record? || @force_update - user_id = @integration.extract_origin_user_id(@token) - return user_id if user_id.failure? + origin_result = @integration.extract_origin_user_id(@token) - @model.origin_user_id = user_id.result + user_id = origin_result.value_or { return ServiceResult.failure(errors: it) } + + @model.origin_user_id = user_id return success unless @model.changed? return failure unless @model.save diff --git a/lib/api/errors/outbound_request_forbidden.rb b/lib/api/errors/outbound_request_forbidden.rb index b146f250554..bd9691591ce 100644 --- a/lib/api/errors/outbound_request_forbidden.rb +++ b/lib/api/errors/outbound_request_forbidden.rb @@ -35,7 +35,7 @@ module API code 500 def initialize(message = I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 403)) - super + super(message || I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 403)) end end end diff --git a/lib/api/errors/outbound_request_not_found.rb b/lib/api/errors/outbound_request_not_found.rb index 3f4ba15952c..b33e38bcfe1 100644 --- a/lib/api/errors/outbound_request_not_found.rb +++ b/lib/api/errors/outbound_request_not_found.rb @@ -35,7 +35,7 @@ module API code 500 def initialize(message = I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 404)) - super + super(message || I18n.t("api_v3.errors.code_500_outbound_request_failure", status_code: 404)) end end end diff --git a/lib/api/errors/unauthenticated.rb b/lib/api/errors/unauthenticated.rb index a9c620ce7b4..6dd7db09c93 100644 --- a/lib/api/errors/unauthenticated.rb +++ b/lib/api/errors/unauthenticated.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -33,7 +35,7 @@ module API code 401 def initialize(message = I18n.t("api_v3.errors.code_401")) - super + super(message || I18n.t("api_v3.errors.code_401")) end end end diff --git a/modules/reporting/spec/models/cost_query/result_spec.rb b/modules/reporting/spec/models/cost_query/results_spec.rb similarity index 100% rename from modules/reporting/spec/models/cost_query/result_spec.rb rename to modules/reporting/spec/models/cost_query/results_spec.rb diff --git a/modules/storages/app/common/storages/adapters/adapter_types.rb b/modules/storages/app/common/storages/adapters/adapter_types.rb new file mode 100644 index 00000000000..2144c0191d1 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/adapter_types.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module AdapterTypes + include Dry.Types() + + # We need to move the definition of ParentFolder to mean something like Folder + Location = AdapterTypes.Constructor(Peripherals::ParentFolder) + StorageFileInstance = AdapterTypes.Instance(Results::StorageFile) + SemanticVersionType = AdapterTypes.Constructor(SemanticVersion, SemanticVersion.method(:parse)) + HTTPVerb = AdapterTypes::Nominal::Symbol.constrained(included_in: %i(post put)) + end + end +end diff --git a/modules/storages/app/common/storages/adapters/authentication.rb b/modules/storages/app/common/storages/adapters/authentication.rb new file mode 100644 index 00000000000..49bf6431446 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/authentication.rb @@ -0,0 +1,88 @@ +# 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 Adapters + class Authentication + class << self + # @param strategy [Input::Strategy] + # @return [AuthenticationStrategy] + # rubocop:disable Metrics/AbcSize + def [](strategy) + auth = strategy.value_or { |it| raise ArgumentError, "Invalid authentication strategy '#{it.inspect}'" } + + case auth.key + when :noop + AuthenticationStrategies::Noop.new + when :basic_auth + AuthenticationStrategies::BasicAuth.new + when :bearer_token + AuthenticationStrategies::BearerToken.new(auth.token) + when :oauth_user_token + AuthenticationStrategies::OAuthUserToken.new(auth.user) + when :oauth_client_credentials + AuthenticationStrategies::OAuthClientCredentials.new(auth.use_cache) + when :sso_user_token + AuthenticationStrategies::SsoUserToken.new(auth.user) + else + raise Errors::UnknownAuthenticationStrategy, "Unknown #{auth.key} authentication scheme" + end + end + # rubocop:enable Metrics/AbcSize + + # TODO: Needs update for OIDC. Add tests for this. + # Used only on the API. Should it become a service? - 2025-01-15 @mereghost + def authorization_state(storage:, user:) + auth_strategy = Registry["#{storage}.authentication.user_bound"].call(user, storage) + + Registry.resolve("#{storage}.queries.user") + .call(storage:, auth_strategy:) + .either( + ->(*) { :connected }, + ->(error) { handle_error(error) } + ) + end + + private + + def handle_error(error) + case error.code + when :unauthorized + :failed_authorization + when :missing_token + :not_connected + else + :error + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/authentication_strategies/authentication_strategy.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/authentication_strategy.rb new file mode 100644 index 00000000000..3c391fcba23 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/authentication_strategy.rb @@ -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 + module Adapters + module AuthenticationStrategies + class AuthenticationStrategy + include TaggedLogging + include Dry::Monads[:result] + + def call(**) + raise Errors::SubclassResponsibility + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/specific_bearer_token.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/basic_auth.rb similarity index 64% rename from modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/specific_bearer_token.rb rename to modules/storages/app/common/storages/adapters/authentication_strategies/basic_auth.rb index 08d8ba49a15..cea3bb61c9a 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/specific_bearer_token.rb +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/basic_auth.rb @@ -29,25 +29,24 @@ #++ module Storages - module Peripherals - module StorageInteraction - module AuthenticationStrategies - class SpecificBearerToken - def self.strategy - Strategy.new(:bearer_token) - end + module Adapters + module AuthenticationStrategies + # Authenticates on a Storage Provider using Basic Auth. + # It expects that the [Storage] to have a [#username] and [#password] set onto it. + class BasicAuth < AuthenticationStrategy + def call(storage:, http_options: {}) + username = storage.username + password = storage.password - attr_reader :bearer_token + return build_failure(storage) if username.blank? || password.blank? - def initialize(bearer_token) - @bearer_token = bearer_token - end + yield OpenProject.httpx.basic_auth(username, password).with(http_options) + end - # rubocop:disable Lint/UnusedMethodArgument - def call(storage:, http_options: {}) - yield OpenProject.httpx.bearer_auth(bearer_token).with(http_options) - end - # rubocop:enable Lint/UnusedMethodArgument + private + + def build_failure(storage) + Failure(Results::Error.new(source: self.class, payload: storage, code: :missing_credentials)) end end end diff --git a/modules/storages/app/common/storages/adapters/authentication_strategies/bearer_token.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/bearer_token.rb new file mode 100644 index 00000000000..4753b2e591a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/bearer_token.rb @@ -0,0 +1,46 @@ +# 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 Adapters + module AuthenticationStrategies + class BearerToken < AuthenticationStrategy + def initialize(token) + super() + @token = token + end + + def call(http_options: {}, **) + yield OpenProject.httpx.bearer_auth(@token).with(http_options) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/noop.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/noop.rb similarity index 76% rename from modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/noop.rb rename to modules/storages/app/common/storages/adapters/authentication_strategies/noop.rb index e57cecdfe40..71802b66afb 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/noop.rb +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/noop.rb @@ -29,20 +29,11 @@ #++ module Storages - module Peripherals - module StorageInteraction - module AuthenticationStrategies - class Noop - def self.strategy - Strategy.new(:noop) - end - - # rubocop:disable Lint/UnusedMethodArgument - def call(storage:, http_options: {}) - yield OpenProject.httpx.with(http_options) - end - - # rubocop:enable Lint/UnusedMethodArgument + module Adapters + module AuthenticationStrategies + class Noop < AuthenticationStrategy + def call(http_options: {}, **) + yield OpenProject.httpx.with(http_options) end end end diff --git a/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_client_credentials.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_client_credentials.rb new file mode 100644 index 00000000000..ac771777fdd --- /dev/null +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_client_credentials.rb @@ -0,0 +1,109 @@ +# 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 Adapters + module AuthenticationStrategies + class OAuthClientCredentials < AuthenticationStrategy + TOKEN_CACHE_KEY = "storage.%s.httpx_access_token" + + def initialize(use_cache) + super() + @use_cache = use_cache + end + + def call(storage:, http_options: {}) + config = validate_configuration(storage).value_or { return Failure(it) } + + token_cache_key = TOKEN_CACHE_KEY % storage.id + access_token = @use_cache ? Rails.cache.read(token_cache_key) : nil + + http = build_http_session(access_token, config, http_options).value_or { return Failure(it) } + + operation_result = yield http + + return operation_result unless @use_cache + + case operation_result + in Success if @use_cache && access_token.blank? + write_cache(token_cache_key, http) + in Failure(code: :forbidden) + clear_cache(token_cache_key) + else + return operation_result + end + + operation_result + end + + private + + def validate_configuration(storage) + config = storage.oauth_configuration.to_httpx_oauth_config + return Success(config) if config.valid? + + Failure(Results::Error.new(source: self.class, payload: storage, code: :storage_not_configured)) + end + + def write_cache(key, httpx_session) + access_token = httpx_session.instance_variable_get(:@options).oauth_session.access_token + Rails.cache.write(key, access_token, expires_in: 50.minutes) + end + + def clear_cache(key) = Rails.cache.delete(key) + + def build_http_session(access_token, config, http_options) + if access_token.present? + http_with_current_token(access_token:, http_options:) + else + http_with_new_token(config:, http_options:) + end + end + + def http_with_current_token(access_token:, http_options:) + opts = http_options.deep_merge({ headers: { "Authorization" => "Bearer #{access_token}" } }) + Success(OpenProject.httpx.with(opts)) + end + + def http_with_new_token(config:, http_options:) + http = OpenProject.httpx + .oauth_auth(**config.to_h, token_endpoint_auth_method: "client_secret_post") + .with_access_token + .with(http_options) + Success(http) + rescue HTTPX::HTTPError => e + Failure(Results::Error.new(code: :unauthorized, payload: e.response, source: self.class)) + rescue HTTPX::TimeoutError => e + Failure(Results::Error.new(code: :timeout, payload: e.to_s, source: self.class)) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_configuration.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_configuration.rb new file mode 100644 index 00000000000..3eb74f53a28 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_configuration.rb @@ -0,0 +1,55 @@ +# 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 Adapters + module AuthenticationStrategies + class OAuthConfiguration + include ActiveModel::Validations + + attr_reader :scope, :issuer, :client_secret, :client_id + + validates_presence_of :client_id, :client_secret, :issuer + + # FIXME: Move to Data and a Validator + def initialize(client_id: nil, client_secret: nil, issuer: nil, scope: nil) + @client_id = client_id + @client_secret = client_secret + @issuer = issuer + @scope = scope + end + + def to_h + { issuer:, client_id:, client_secret:, scope: } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_user_token.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_user_token.rb new file mode 100644 index 00000000000..eeef435c333 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_user_token.rb @@ -0,0 +1,115 @@ +# 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 Adapters + module AuthenticationStrategies + class OAuthUserToken < AuthenticationStrategy + def initialize(user) + super() + @user = user + @retried = false + @error_data = Results::Error.new(source: self.class, code: :error) + end + + # rubocop:disable Metrics/AbcSize + def call(storage:, http_options: {}, &) + oauth_client = validate_oauth_client(storage).value_or { return Failure(it) } + token = current_token(oauth_client).value_or { return Failure(it) } + + options = http_options.deep_merge(headers: { "Authorization" => "Bearer #{token.access_token}" }) + + original_response = yield OpenProject.httpx.with(options) + + case original_response + in Failure(code: :unauthorized) + refresh_and_retry(storage, token, options, &) + else + original_response + end + rescue ActiveRecord::StaleObjectError => e + raise e if @retried + + Rails.logger.error("#{e.inspect} happened for User ##{@user.id} #{@user.name}") + @retried = true + retry + end + # rubocop:enable Metrics/AbcSize + + private + + def validate_oauth_client(storage) + return Success(storage.oauth_client) if storage.oauth_client + + Failure(@error_data.with(code: :missing_oauth_client, payload: storage)) + end + + def refresh_and_retry(storage, token, options, &) + config = storage.oauth_configuration.to_httpx_oauth_config.to_h + refreshed_session = refresh_token(config, options, token).value_or { return Failure(it) } + update_token(token, refreshed_session) + + yield refreshed_session + end + + def update_token(token, http_session) + oauth_session = http_session.instance_variable_get(:@options).oauth_session + token.update(access_token: oauth_session.access_token, refresh_token: oauth_session.refresh_token) + end + + def refresh_token(oauth_config, options, token) + httpx_config = oauth_config.merge(refresh_token: token.refresh_token, token_endpoint_auth_method: "client_secret_post") + Success(OpenProject.httpx.oauth_auth(**httpx_config).with(options).with_access_token) + rescue HTTPX::HTTPError => e + handle_http_error(token, e) + rescue HTTPX::TimeoutError => e + handle_timeout(token, e) + end + + def handle_timeout(token, exception) + Rails.logger.error("Timeout while refreshing OAuth token. - Payload: #{exception.message}") + token.destroy + Failure(@error_data.with(error: :timeout_on_refresh, payload: exception)) + end + + def handle_http_error(token, error) + Rails.logger.error("Error while refreshing OAuth token - Payload: #{error.response}") + token.destroy + Failure(@error_data.with(code: :unauthorized, payload: error.response)) + end + + def current_token(client) + token = OAuthClientToken.find_by(user: @user, oauth_client: client) + token ? Success(token) : Failure(@error_data.with(code: :missing_token)) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_configuration.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/sso_user_token.rb similarity index 65% rename from modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_configuration.rb rename to modules/storages/app/common/storages/adapters/authentication_strategies/sso_user_token.rb index 068515c5864..d4c92bb0758 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_configuration.rb +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/sso_user_token.rb @@ -29,29 +29,25 @@ #++ module Storages - module Peripherals - module StorageInteraction - module AuthenticationStrategies - class OAuthConfiguration - include ActiveModel::Validations + module Adapters + module AuthenticationStrategies + class SsoUserToken < AuthenticationStrategy + def initialize(user) + super() + @user = user + end - attr_reader :scope, :issuer, :client_secret, :client_id + def call(storage:, http_options: {}, &) + result = OpenIDConnect::UserTokens::FetchService.new(user: @user).access_token_for(audience: storage.audience) - validates_presence_of :client_id, :client_secret, :issuer + token = result.value_or do |failure| + error("Failed to fetch access token for user #{@user}. Error: #{failure.inspect}") - def initialize(client_id: nil, - client_secret: nil, - issuer: nil, - scope: nil) - @client_id = client_id - @client_secret = client_secret - @issuer = issuer - @scope = scope + return Failure(failure.with(code: :unauthorized)) end - def to_h - { issuer:, client_id:, client_secret:, scope: } - end + opts = http_options.deep_merge({ headers: { "Authorization" => "Bearer #{token}" } }) + yield OpenProject.httpx.with(opts) end end end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/base_connection_validator.rb b/modules/storages/app/common/storages/adapters/connection_validators/base_connection_validator.rb similarity index 99% rename from modules/storages/app/common/storages/peripherals/connection_validators/base_connection_validator.rb rename to modules/storages/app/common/storages/adapters/connection_validators/base_connection_validator.rb index f7735efb6f0..bd2030a1d05 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/base_connection_validator.rb +++ b/modules/storages/app/common/storages/adapters/connection_validators/base_connection_validator.rb @@ -29,7 +29,7 @@ #++ module Storages - module Peripherals + module Adapters module ConnectionValidators class BaseConnectionValidator class << self diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/base_validator_group.rb b/modules/storages/app/common/storages/adapters/connection_validators/base_validator_group.rb similarity index 93% rename from modules/storages/app/common/storages/peripherals/connection_validators/base_validator_group.rb rename to modules/storages/app/common/storages/adapters/connection_validators/base_validator_group.rb index 5d0094767df..f5db8f0bdd6 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/base_validator_group.rb +++ b/modules/storages/app/common/storages/adapters/connection_validators/base_validator_group.rb @@ -29,7 +29,7 @@ #++ module Storages - module Peripherals + module Adapters module ConnectionValidators class BaseValidatorGroup include TaggedLogging @@ -38,7 +38,7 @@ module Storages new(storage).call end - def self.key = raise Errors::SubclassResponsibility + def self.key = raise ::Storages::Errors::SubclassResponsibility def initialize(storage) @storage = storage @@ -55,7 +55,7 @@ module Storages private - def validate = raise Errors::SubclassResponsibility + def validate = raise ::Storages::Errors::SubclassResponsibility def register_checks(*keys) keys.each { @results.register_check(it) } diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/check_result.rb b/modules/storages/app/common/storages/adapters/connection_validators/check_result.rb similarity index 99% rename from modules/storages/app/common/storages/peripherals/connection_validators/check_result.rb rename to modules/storages/app/common/storages/adapters/connection_validators/check_result.rb index 0f1641ca844..e375efcb1e3 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/check_result.rb +++ b/modules/storages/app/common/storages/adapters/connection_validators/check_result.rb @@ -29,7 +29,7 @@ #++ module Storages - module Peripherals + module Adapters module ConnectionValidators CheckResult = Data.define(:key, :state, :code, :timestamp, :context) do private_class_method :new diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/validation_group_result.rb b/modules/storages/app/common/storages/adapters/connection_validators/validation_group_result.rb similarity index 99% rename from modules/storages/app/common/storages/peripherals/connection_validators/validation_group_result.rb rename to modules/storages/app/common/storages/adapters/connection_validators/validation_group_result.rb index 36e6b887ebc..06aadf9fd91 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/validation_group_result.rb +++ b/modules/storages/app/common/storages/adapters/connection_validators/validation_group_result.rb @@ -29,7 +29,7 @@ #++ module Storages - module Peripherals + module Adapters module ConnectionValidators class ValidationGroupResult delegate :[], :each_pair, to: :@results diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/validator_result.rb b/modules/storages/app/common/storages/adapters/connection_validators/validator_result.rb similarity index 99% rename from modules/storages/app/common/storages/peripherals/connection_validators/validator_result.rb rename to modules/storages/app/common/storages/adapters/connection_validators/validator_result.rb index 563dbbc5445..526ebb0ebbb 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/validator_result.rb +++ b/modules/storages/app/common/storages/adapters/connection_validators/validator_result.rb @@ -29,7 +29,7 @@ #++ module Storages - module Peripherals + module Adapters module ConnectionValidators class ValidatorResult private attr_reader :group_results diff --git a/modules/storages/app/common/storages/adapters/errors.rb b/modules/storages/app/common/storages/adapters/errors.rb new file mode 100644 index 00000000000..ee6187a4d46 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/errors.rb @@ -0,0 +1,59 @@ +# 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 Adapters + module Errors + ResolverStandardError = Class.new(::Storages::Errors::BaseError) + MissingContract = Class.new(ResolverStandardError) + OperationNotSupported = Class.new(ResolverStandardError) + MissingModel = Class.new(ResolverStandardError) + UnknownProvider = Class.new(ResolverStandardError) + UnknownAuthenticationStrategy = Class.new(ArgumentError) + + def self.registry_error_for(key) + case key.split(".") + in [storage, *] if Registry.known_providers.exclude?(storage) + UnknownProvider.new(storage) + in [storage, "contracts", model] + MissingContract.new("No #{model} contract defined for provider: #{storage.camelize}") + in [storage, "commands" | "queries" => type, operation] + OperationNotSupported.new( + "#{type.singularize.capitalize} #{operation} not supported by provider: #{storage.camelize}" + ) + in [storage, "models", object] + MissingModel.new("Model #{object} not registered for provider: #{storage.camelize}") + else + ResolverStandardError.new("Cannot resolve key #{key}.") + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/add_user_to_group.rb b/modules/storages/app/common/storages/adapters/input/add_user_to_group.rb new file mode 100644 index 00000000000..415872c0f74 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/add_user_to_group.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + AddUserToGroup = Data.define(:group, :user) do + private_class_method :new + + def self.build(group:, user:, contract: AddUserToGroupContract.new) + contract.call(group:, user:).to_monad.fmap { new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/result_data/copy_template_folder.rb b/modules/storages/app/common/storages/adapters/input/add_user_to_group_contract.rb similarity index 86% rename from modules/storages/app/common/storages/peripherals/storage_interaction/result_data/copy_template_folder.rb rename to modules/storages/app/common/storages/adapters/input/add_user_to_group_contract.rb index 160f4e05a11..f3a9b7b9bff 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/result_data/copy_template_folder.rb +++ b/modules/storages/app/common/storages/adapters/input/add_user_to_group_contract.rb @@ -29,11 +29,12 @@ #++ module Storages - module Peripherals - module StorageInteraction - module ResultData - CopyTemplateFolder = Data.define(:id, :polling_url, :requires_polling) do - def requires_polling? = !!requires_polling + module Adapters + module Input + class AddUserToGroupContract < Dry::Validation::Contract + params do + required(:user).filled(:string) + required(:group).filled(:string) end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/delete_folder_command.rb b/modules/storages/app/common/storages/adapters/input/copy_template_folder.rb similarity index 76% rename from modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/delete_folder_command.rb rename to modules/storages/app/common/storages/adapters/input/copy_template_folder.rb index 5dbb4e52b32..c9d0f3655ba 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/delete_folder_command.rb +++ b/modules/storages/app/common/storages/adapters/input/copy_template_folder.rb @@ -28,18 +28,16 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Peripherals::StorageInteraction::Nextcloud - class DeleteFolderCommand - def self.call(storage:, auth_strategy:, location:) - new(storage).call(auth_strategy:, location:) - end +module Storages + module Adapters + module Input + CopyTemplateFolder = Data.define(:source, :destination) do + private_class_method :new - def initialize(storage) - @delegate = Internal::DeleteEntityCommand.new(storage) - end - - def call(auth_strategy:, location:) - @delegate.call(auth_strategy:, location:) + def self.build(source:, destination:, contract: CopyTemplateFolderContract.new) + contract.call(source:, destination:).to_monad.fmap { new(**it.to_h) } + end + end end end end diff --git a/modules/storages/app/common/storages/adapters/input/copy_template_folder_contract.rb b/modules/storages/app/common/storages/adapters/input/copy_template_folder_contract.rb new file mode 100644 index 00000000000..591110281fa --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/copy_template_folder_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Input + class CopyTemplateFolderContract < Dry::Validation::Contract + params do + required(:source).filled(:string) + required(:destination).filled(:string) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/create_folder.rb b/modules/storages/app/common/storages/adapters/input/create_folder.rb new file mode 100644 index 00000000000..ff161f40864 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/create_folder.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + CreateFolder = Data.define(:folder_name, :parent_location) do + private_class_method :new + + def self.build(folder_name:, parent_location:, contract: CreateFolderContract.new) + contract.call(folder_name:, parent_location:).to_monad.fmap { |it| new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/create_folder_contract.rb b/modules/storages/app/common/storages/adapters/input/create_folder_contract.rb new file mode 100644 index 00000000000..2e636ec955e --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/create_folder_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Input + class CreateFolderContract < Dry::Validation::Contract + params do + required(:folder_name).filled(:string) + required(:parent_location).filter(:filled?, :str?).value(AdapterTypes::Location) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/delete_folder.rb b/modules/storages/app/common/storages/adapters/input/delete_folder.rb new file mode 100644 index 00000000000..fa13666b6f5 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/delete_folder.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + DeleteFolder = Data.define(:location) do + private_class_method :new + + def self.build(location:, contract: DeleteFolderContract.new) + contract.call(location:).to_monad.fmap { |it| new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/delete_folder_contract.rb b/modules/storages/app/common/storages/adapters/input/delete_folder_contract.rb new file mode 100644 index 00000000000..11dff8c46bf --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/delete_folder_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Input + class DeleteFolderContract < Dry::Validation::Contract + params do + # FIXME: Should this be a Location? + required(:location).filled(:string) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/download_link.rb b/modules/storages/app/common/storages/adapters/input/download_link.rb new file mode 100644 index 00000000000..5b5cfbc4ed1 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/download_link.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + DownloadLink = Data.define(:file_link) do + private_class_method :new + + def self.build(file_link:, contract: DownloadLinkContract.new) + contract.call(file_link:).to_monad.fmap { |it| new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/contracts/storages/storages/nextcloud_contract.rb b/modules/storages/app/common/storages/adapters/input/download_link_contract.rb similarity index 84% rename from modules/storages/app/contracts/storages/storages/nextcloud_contract.rb rename to modules/storages/app/common/storages/adapters/input/download_link_contract.rb index cd45897f685..f0d0531a1f4 100644 --- a/modules/storages/app/contracts/storages/storages/nextcloud_contract.rb +++ b/modules/storages/app/common/storages/adapters/input/download_link_contract.rb @@ -28,10 +28,14 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::Storages - class NextcloudContract < ComposedContract - include_contract NextcloudGeneralInformationContract - include_contract NextcloudAudienceContract - include_contract NextcloudAutomaticManagementContract +module Storages + module Adapters + module Input + class DownloadLinkContract < Dry::Validation::Contract + params do + required(:file_link).filled(type?: FileLink) + end + end + end end end diff --git a/modules/storages/app/common/storages/adapters/input/file_info.rb b/modules/storages/app/common/storages/adapters/input/file_info.rb new file mode 100644 index 00000000000..6392a7c07ba --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/file_info.rb @@ -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 + module Adapters + module Input + # FIXME: Should FileID become a Location? + FileInfo = Data.define(:file_id) do + private_class_method :new + + def self.build(file_id:, contract: FileInfoContract.new) + contract.call(file_id:).to_monad.fmap { |it| new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/file_info_contract.rb b/modules/storages/app/common/storages/adapters/input/file_info_contract.rb new file mode 100644 index 00000000000..e0b3bb554d0 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/file_info_contract.rb @@ -0,0 +1,41 @@ +# 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 Adapters + module Input + class FileInfoContract < Dry::Validation::Contract + params do + required(:file_id).filled(:string) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/file_path_to_id_map.rb b/modules/storages/app/common/storages/adapters/input/file_path_to_id_map.rb new file mode 100644 index 00000000000..3758c0cff64 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/file_path_to_id_map.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + FilePathToIdMap = Data.define(:folder, :depth) do + private_class_method :new + + def self.build(folder:, depth: Float::INFINITY, contract: FilePathToIdMapContract.new) + contract.call(folder:, depth:).to_monad.fmap { |it| new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/file_path_to_id_map_contract.rb b/modules/storages/app/common/storages/adapters/input/file_path_to_id_map_contract.rb new file mode 100644 index 00000000000..55d239e3f38 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/file_path_to_id_map_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Input + class FilePathToIdMapContract < Dry::Validation::Contract + params do + required(:folder).filter(:filled?, :str?).value(AdapterTypes::Location) + required(:depth).filled { (int? & gteq?(0)) | eql?(Float::INFINITY) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/files.rb b/modules/storages/app/common/storages/adapters/input/files.rb new file mode 100644 index 00000000000..aeac119ec1a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/files.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + Files = Data.define(:folder) do + private_class_method :new + + def self.build(folder:, contract: FilesContract.new) + contract.call(folder:).to_monad.fmap { |it| new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/files_contract.rb b/modules/storages/app/common/storages/adapters/input/files_contract.rb new file mode 100644 index 00000000000..d6d971c64d4 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/files_contract.rb @@ -0,0 +1,41 @@ +# 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 Adapters + module Input + class FilesContract < Dry::Validation::Contract + params do + required(:folder).filter(:filled?, :str?).value(AdapterTypes::Location) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/files_info.rb b/modules/storages/app/common/storages/adapters/input/files_info.rb new file mode 100644 index 00000000000..2b95f2d9bf1 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/files_info.rb @@ -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 + module Adapters + module Input + # FIXME: Should FileIDs become a Array(Location)? + FilesInfo = Data.define(:file_ids) do + private_class_method :new + + def self.build(file_ids:, contract: FilesInfoContract.new) + contract.call(file_ids:).to_monad.fmap { |it| new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/files_info_contract.rb b/modules/storages/app/common/storages/adapters/input/files_info_contract.rb new file mode 100644 index 00000000000..d0e3615f996 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/files_info_contract.rb @@ -0,0 +1,41 @@ +# 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 Adapters + module Input + class FilesInfoContract < Dry::Validation::Contract + params do + required(:file_ids).array(:string) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/group_users.rb b/modules/storages/app/common/storages/adapters/input/group_users.rb new file mode 100644 index 00000000000..c1e6a78f216 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/group_users.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + GroupUsers = Data.define(:group) do + private_class_method :new + + def self.build(group:, contract: GroupUsersContract.new) + contract.call(group:).to_monad.fmap { new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/group_users_contract.rb b/modules/storages/app/common/storages/adapters/input/group_users_contract.rb new file mode 100644 index 00000000000..8caf6f6860f --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/group_users_contract.rb @@ -0,0 +1,41 @@ +# 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 Adapters + module Input + class GroupUsersContract < Dry::Validation::Contract + params do + required(:group).filled(:string) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/open_file_link.rb b/modules/storages/app/common/storages/adapters/input/open_file_link.rb new file mode 100644 index 00000000000..4c627168b83 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/open_file_link.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + OpenFileLink = Data.define(:file_id, :open_location) do + private_class_method :new + + def self.build(file_id:, open_location: false, contract: OpenFileLinkContract.new) + contract.call(file_id:, open_location:).to_monad.fmap { new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/open_file_link_contract.rb b/modules/storages/app/common/storages/adapters/input/open_file_link_contract.rb new file mode 100644 index 00000000000..904bc5e3701 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/open_file_link_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Input + class OpenFileLinkContract < Dry::Validation::Contract + params do + required(:file_id).filled(:string) + required(:open_location).maybe(:bool) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/remove_user_from_group.rb b/modules/storages/app/common/storages/adapters/input/remove_user_from_group.rb new file mode 100644 index 00000000000..ded3ed317d6 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/remove_user_from_group.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + RemoveUserFromGroup = Data.define(:group, :user) do + private_class_method :new + + def self.build(group:, user:, contract: RemoveUserFromGroupContract.new) + contract.call(group:, user:).to_monad.fmap { new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/remove_user_from_group_contract.rb b/modules/storages/app/common/storages/adapters/input/remove_user_from_group_contract.rb new file mode 100644 index 00000000000..8ee629eaeb3 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/remove_user_from_group_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Input + class RemoveUserFromGroupContract < Dry::Validation::Contract + params do + required(:user).filled(:string) + required(:group).filled(:string) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/rename_file.rb b/modules/storages/app/common/storages/adapters/input/rename_file.rb new file mode 100644 index 00000000000..fc5a023036d --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/rename_file.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + RenameFile = Data.define(:location, :new_name) do + private_class_method :new + + def self.build(location:, new_name:, contract: RenameFileContract.new) + contract.call(location:, new_name:).to_monad.fmap { |it| new(**it.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/rename_file_contract.rb b/modules/storages/app/common/storages/adapters/input/rename_file_contract.rb new file mode 100644 index 00000000000..23dc724d91a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/rename_file_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Input + class RenameFileContract < Dry::Validation::Contract + params do + required(:new_name).filled(:string) + required(:location).filter(:filled?, :str?).value(AdapterTypes::Location) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/set_permissions.rb b/modules/storages/app/common/storages/adapters/input/set_permissions.rb new file mode 100644 index 00000000000..25a36ada07a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/set_permissions.rb @@ -0,0 +1,56 @@ +# 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 Adapters + module Input + # user_permissions - A list of user specific file permissions. + # IMPORTANT: the user ids are considered to be the ids of the remote identities. If user permissions should be + # set via a group, a `group_id` must be provided instead of a `user_id`. + # Example: + # [ + # {user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [ :read_files, + # :write_files, + # :create_files, + # :delete_files, + # :share_files ]}, + # {user_id: "f6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files, :write_files]}, + # {group_id: "fee9cd49-17e2-4430-9235-2060e7372568", permissions: [:read_files]}, + # ] + SetPermissions = ::Data.define(:file_id, :user_permissions) do + private_class_method :new + + def self.build(file_id:, user_permissions:, contract: SetPermissionsContract.new) + contract.call(file_id:, user_permissions:).to_monad.fmap { |result| new(**result.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/set_permissions_contract.rb b/modules/storages/app/common/storages/adapters/input/set_permissions_contract.rb new file mode 100644 index 00000000000..d101b3335cd --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/set_permissions_contract.rb @@ -0,0 +1,54 @@ +# 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 Adapters + module Input + class SetPermissionsContract < Dry::Validation::Contract + params do + required(:file_id).filled(:string) + required(:user_permissions).array(:hash) do + optional(:user_id).filled(:string) + optional(:group_id).filled(:string) + required(:permissions) + .array(:symbol, included_in?: OpenProject::Storages::Engine.external_file_permissions) + end + end + + rule(:user_permissions).each do + both = value.key?(:user_id) && value.key?(:group_id) + none = !value.key?(:user_id) && !value.key?(:group_id) + + key.failure("must have either user_id or group_id") if both || none + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/strategy.rb b/modules/storages/app/common/storages/adapters/input/strategy.rb new file mode 100644 index 00000000000..8f77f253f3f --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/strategy.rb @@ -0,0 +1,55 @@ +# 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 Adapters + module Input + Strategy = Data.define(:key, :user, :storage, :use_cache, :token) do + private_class_method :new + + def self.build(key:, user: nil, storage: nil, use_cache: true, token: nil, contract: StrategyContract.new) + contract.call(key:, user:, use_cache:, storage:, token:).to_monad.fmap { |result| new(**result.to_h) } + end + + def with_user(user) + with(user:) + end + + def with_cache(use_cache) + with(use_cache:) + end + + def with_token(token) + with(token:) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/managed_folder_identifier/one_drive.rb b/modules/storages/app/common/storages/adapters/input/strategy_contract.rb similarity index 72% rename from modules/storages/app/common/storages/peripherals/managed_folder_identifier/one_drive.rb rename to modules/storages/app/common/storages/adapters/input/strategy_contract.rb index 93563cf331f..9b7149660b0 100644 --- a/modules/storages/app/common/storages/peripherals/managed_folder_identifier/one_drive.rb +++ b/modules/storages/app/common/storages/adapters/input/strategy_contract.rb @@ -29,26 +29,17 @@ #++ module Storages - module Peripherals - module ManagedFolderIdentifier - class OneDrive - CHARACTER_BLOCKLIST = /[\\<>+?:"|\/]/ + module Adapters + module Input + class StrategyContract < Dry::Validation::Contract + AUTH_METHODS = %i[noop basic_auth oauth_client_credentials oauth_user_token sso_user_token bearer_token].to_set.freeze - def initialize(project_storage) - @project_storage = project_storage - @project = project_storage.project - end - - def name - path - end - - def path - "#{@project.name.gsub(CHARACTER_BLOCKLIST, '_')} (#{@project.id})" - end - - def location - @project_storage.project_folder_id + params do + required(:key).filled(:symbol, included_in?: AUTH_METHODS) + optional(:user).maybe(type?: User) + optional(:storage).maybe(type?: Storage) + optional(:use_cache).maybe(:bool) + optional(:token).maybe(:string) end end end diff --git a/modules/storages/app/common/storages/adapters/input/upload_link.rb b/modules/storages/app/common/storages/adapters/input/upload_link.rb new file mode 100644 index 00000000000..8fd64a7c5c3 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/upload_link.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Input + UploadLink = Data.define(:folder_id, :file_name) do + private_class_method :new + + def self.build(folder_id:, file_name:, contract: UploadLinkContract.new) + contract.call(folder_id:, file_name:).to_monad.fmap { |result| new(**result.to_h) } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/input/upload_link_contract.rb b/modules/storages/app/common/storages/adapters/input/upload_link_contract.rb new file mode 100644 index 00000000000..54763be553b --- /dev/null +++ b/modules/storages/app/common/storages/adapters/input/upload_link_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Input + class UploadLinkContract < Dry::Validation::Contract + params do + required(:folder_id).filled(:string) + required(:file_name).filled(:string) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/oauth_configuration_base.rb b/modules/storages/app/common/storages/adapters/oauth_configuration_base.rb new file mode 100644 index 00000000000..4f1ef8050ea --- /dev/null +++ b/modules/storages/app/common/storages/adapters/oauth_configuration_base.rb @@ -0,0 +1,45 @@ +# 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 Adapters + class OAuthConfigurationBase + def scope = raise ::Storages::Errors::SubclassResponsibility + + def basic_rack_oauth_client = raise ::Storages::Errors::SubclassResponsibility + + def to_httpx_oauth_config = raise ::Storages::Errors::SubclassResponsibility + + def authorization_uri(state: nil) + basic_rack_oauth_client.authorization_uri(scope:, state:) + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/base.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/base.rb new file mode 100644 index 00000000000..1e33b2d7506 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/base.rb @@ -0,0 +1,85 @@ +# 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 Adapters + module Providers + module Nextcloud + class Base + include TaggedLogging + include Dry::Monads::Result(Results::Error) + + def self.call(storage:, auth_strategy:, input_data:) + new(storage).call(auth_strategy:, input_data:) + end + + def initialize(storage) + @storage = storage + end + + private + + def ocs_api_request_headers = { headers: { "OCS-APIRequest" => "true" } } + def depth_header(depth) = { headers: { "Depth" => depth.to_s } } + + def origin_user_id(auth_strategy:) + error = Results::Error.new(source: self.class, code: :error) + + auth_strategy.bind do |strategy| + case strategy.key + when :basic_auth + Success(@storage.username) + when :oauth_user_token, :sso_user_token + fetch_remote_identity(strategy.user, strategy.key) + else + Failure( + error.with( + payload: "authentication strategy with user context found. Cannot execute query without user context." + ) + ) + end + end + end + + def fetch_remote_identity(user, auth_key) + integration = auth_key == :sso_user_token ? user.authentication_provider : @storage.oauth_client + remote_id = RemoteIdentity.of_user_and_client(user, integration, @storage) + + if remote_id.present? + Success(remote_id.origin_user_id) + else + Failure(error.with(payload: "No origin user ID or user token found. Cannot execute query without user context.")) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/add_user_to_group_command.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/add_user_to_group_command.rb new file mode 100644 index 00000000000..9ea95f3e469 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/add_user_to_group_command.rb @@ -0,0 +1,98 @@ +# 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 Adapters + module Providers + module Nextcloud + module Commands + class AddUserToGroupCommand < Base + def call(auth_strategy:, input_data:) + with_tagged_logger do + Authentication[auth_strategy].call(storage: @storage, http_options: ocs_api_request_headers) do |http| + url = UrlBuilder.url(@storage.uri, "ocs/v1.php/cloud/users", input_data.user, "groups") + info "Adding #{input_data.user} to #{input_data.group} through #{url}" + + response = http.post(url, form: { "groupid" => input_data.group }) + handle_response(response) + end + end + end + + private + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + handle_success_response(response, error) + in { status: 405 } + Failure(error.with(code: :not_allowed)) + in { status: 401 } + Failure(error.with(code: :not_found)) + in { status: 404 } + Failure(error.with(code: :unauthorized)) + in { status: 409 } + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :error)) + end + end + + # rubocop:disable Metrics/AbcSize + def handle_success_response(response, error) + status_code = Nokogiri::XML(response.body.to_s).xpath("/ocs/meta/statuscode").text + + case status_code + when "100" + info "User has been added to the group" + Success() + when "101" + Failure(error.with(code: :no_group_specified)) + when "102" + Failure(error.with(code: :group_does_not_exist)) + when "103" + Failure(error.with(code: :user_does_not_exist)) + when "104" + Failure(error.with(code: :insufficient_privileges)) + when "105" + Failure(error.with(code: :failed_to_add)) + else + Failure(error.with(code: :error)) + end + end + # rubocop:enable Metrics/AbcSize + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/copy_template_folder_command.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/copy_template_folder_command.rb new file mode 100644 index 00000000000..cfb073b192d --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/copy_template_folder_command.rb @@ -0,0 +1,136 @@ +# 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 Adapters + module Providers + module Nextcloud + module Commands + class CopyTemplateFolderCommand < Base + def initialize(storage) + super + @data = Results::CopyTemplateFolder.new(id: nil, polling_url: nil, requires_polling: false) + end + + def call(auth_strategy:, input_data:) + with_tagged_logger do + Authentication[auth_strategy].call(storage: @storage) do |http| + remote_urls = build_origin_urls(input_data) + + ensure_remote_folder_does_not_exist(http, remote_urls[:destination_url]).bind do + copy_folder(http, **remote_urls).bind do + get_folder_id(auth_strategy, input_data.destination) + end + end + end + end + end + + private + + def validate_inputs(source_path, destination_path) + info "Validating #{source_path} and #{destination_path}" + if source_path.blank? || destination_path.blank? + return Util.error(:missing_paths, "Source and destination paths must be present.") + end + + ServiceResult.success(result: { source_path:, destination_path: }) + end + + def build_origin_urls(input_data) + source_url = UrlBuilder.url(@storage.uri, "remote.php/dav/files", @storage.username, input_data.source) + destination_url = UrlBuilder.url(@storage.uri, "remote.php/dav/files", @storage.username, input_data.destination) + + { source_url:, destination_url: } + end + + def ensure_remote_folder_does_not_exist(http, destination_url) + info "Checking if #{destination_url} does not already exists." + response = http.head(destination_url) + + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + Failure(error.with(code: :conflict)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 404 } + Success() + else + Failure(error.with(code: :error)) + end + end + + def copy_folder(http, source_url:, destination_url:) + info "Copying #{source_url} to #{destination_url}" + handle_response http.request("COPY", + source_url, + headers: { "Destination" => destination_url, "Depth" => "infinity" }) + end + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + Success() + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 409 } + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :error)) + end + end + + def get_folder_id(auth_strategy, destination_path) + # file_path_to_id_map query returns keys without trailing slashes + # TODO: Harden this with https://community.openproject.org/wp/57850 + sanitized_path = destination_path.chomp("/") + + Input::FilePathToIdMap.build(folder: sanitized_path, depth: 0).bind do |input_data| + Registry.resolve("nextcloud.queries.file_path_to_id_map") + .call(storage: @storage, auth_strategy:, input_data:) + .fmap { @data.with(id: it.fetch(sanitized_path).id) } + end + end + + def source = self.class + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/create_folder_command.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/create_folder_command.rb new file mode 100644 index 00000000000..9cc5b277e80 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/create_folder_command.rb @@ -0,0 +1,126 @@ +# 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 Adapters + module Providers + module Nextcloud + module Commands + class CreateFolderCommand < Base + def call(auth_strategy:, input_data:) + with_tagged_logger do + info "Trying to create folder #{input_data.folder_name} under #{input_data.parent_location}" + origin_user_id(auth_strategy:).bind do |origin_user| + path_prefix = UrlBuilder.path(@storage.uri.path, "remote.php/dav/files", origin_user) + request_url = UrlBuilder.url(@storage.uri, + "remote.php/dav/files", + origin_user, + input_data.parent_location.path, + input_data.folder_name) + + create_folder_request(auth_strategy, request_url, path_prefix) + end + end + end + + private + + def create_folder_request(auth_strategy, request_url, path_prefix) + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response(http.mkcol(request_url)).bind do + handle_response(http.propfind(request_url, requested_properties)).bind do |response| + info "Folder successfully created" + storage_file(path_prefix, response) + end + end + end + end + + def handle_response(response) + error = Results::Error.new(payload: response, source: self.class) + + case response + in { status: 200..299 } + Success(response) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 404 | 409 } # webDAV endpoint returns 409 if path does not exist + Failure(error.with(code: :not_found)) + in { status: 405 } # webDAV endpoint returns 405 if folder already exists + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :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"].permissions + xml["oc"].send(:"owner-display-name") + end + end + end.to_xml + end + + # FIXME: Move this to a transformer? + # 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( + UrlBuilder.path(CGI.unescapeURIComponent(path)).gsub(path_prefix, "") + ).delete_suffix("/") + + Results::StorageFile.build( + 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: + ) + # rubocop:enable Metrics/AbcSize + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/delete_folder_command.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/delete_folder_command.rb new file mode 100644 index 00000000000..6406380f4be --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/delete_folder_command.rb @@ -0,0 +1,67 @@ +# 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 Adapters + module Providers + module Nextcloud + module Commands + class DeleteFolderCommand < Base + def call(auth_strategy:, input_data:) + origin_user_id(auth_strategy:).bind do |origin_user_id| + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response http.delete( + UrlBuilder.url(@storage.uri, "remote.php/dav/files", origin_user_id, input_data.location) + ) + end + end + end + + private + + def handle_response(response) + error = Results::Error.new(payload: response, source: self.class) + case response + in { status: 200..299 } + Success() + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/remove_user_from_group_command.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/remove_user_from_group_command.rb new file mode 100644 index 00000000000..c6982d10494 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/remove_user_from_group_command.rb @@ -0,0 +1,97 @@ +# 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 Adapters + module Providers + module Nextcloud + module Commands + class RemoveUserFromGroupCommand < Base + def call(auth_strategy:, input_data:) + with_tagged_logger do + Authentication[auth_strategy].call(storage: @storage, http_options: ocs_api_request_headers) do |http| + url = UrlBuilder.url(@storage.uri, "ocs/v1.php/cloud/users", input_data.user, "groups") + url << "?groupid=#{CGI.escapeURIComponent(input_data.group)}" + + info "Removing #{input_data.user} from #{input_data.group} through #{url}" + handle_response(http.delete(url)) + end + end + end + + private + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + handle_success_response(response.xml, error) + in { status: 405 } + Failure(error.with(code: :not_allowed)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 409 } + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :error)) + end + end + + # rubocop:disable Metrics/AbcSize + def handle_success_response(response, error) + status_code = response.xpath("/ocs/meta/statuscode").text + case status_code + when "100" + info "User has been removed from group" + Success() + when "101" + Failure(error.with(code: :no_group_specified)) + when "102" + Failure(error.with(code: :group_does_not_exist)) + when "103" + Failure(error.with(code: :user_does_not_exist)) + when "104" + Failure(error.with(code: :insufficient_privileges)) + when "105" + Failure(error.with(code: :failed_to_remove)) + else + Failure(error.with(code: :error)) + end + end + # rubocop:enable Metrics/AbcSize + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/rename_file_command.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/rename_file_command.rb new file mode 100644 index 00000000000..ef8092f9753 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/rename_file_command.rb @@ -0,0 +1,104 @@ +# 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 Adapters + module Providers + module Nextcloud + module Commands + class RenameFileCommand < Base + def initialize(*) + super + @file_info_query = Queries::FileInfoQuery.new(@storage) + end + + # rubocop:disable Metrics/AbcSize + def call(auth_strategy:, input_data:) + with_tagged_logger do + info "Validating user remote ID" + origin_user_id(auth_strategy:).bind do |origin_user_id| + info "Getting the folder information" + Input::FileInfo.build(file_id: input_data.location.path).bind do |info_input_data| + @file_info_query.call(auth_strategy:, input_data: info_input_data).bind do |fileinfo| + info "Renaming the folder #{fileinfo.location} to #{input_data.new_name}" + make_request(auth_strategy, origin_user_id, fileinfo, input_data.new_name).bind do + info "Retrieving updated file info for the #{input_data.new_name} folder" + @file_info_query.call(auth_strategy:, input_data: info_input_data).bind(&:to_storage_file) + end + end + end + end + end + end + # rubocop:enable Metrics/AbcSize + + private + + def make_request(auth_strategy, user, file_info, name) + source_path = UrlBuilder.url(@storage.uri, + "remote.php/dav/files", + user, + CGI.unescape(file_info.location)) + + destination = UrlBuilder.path(@storage.uri.path, + "remote.php/dav/files", + user, + CGI.unescape(target_path(file_info, name))) + + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response http.request("MOVE", source_path, headers: { "Destination" => destination, "Overwrite" => "F" }) + end + end + + def target_path(info, name) + info.location.gsub(CGI.escapeURIComponent(info.name), CGI.escapeURIComponent(name)) + end + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + case response + in { status: 200..299 } + Success() + in { status: 412 } + Failure(error.with(code: :conflict)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/set_permissions_command.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/set_permissions_command.rb new file mode 100644 index 00000000000..814267a52db --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/commands/set_permissions_command.rb @@ -0,0 +1,145 @@ +# 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 Adapters + module Providers + module Nextcloud + module Commands + class SetPermissionsCommand < Base + PERMISSIONS_MAP = { read_files: 1, write_files: 2, create_files: 4, delete_files: 8, share_files: 16 }.freeze + PERMISSIONS_KEYS = OpenProject::Storages::Engine.external_file_permissions + SUCCESS_XPATH = "/d:multistatus/d:response/d:propstat[d:status[text() = 'HTTP/1.1 200 OK']]/d:prop/nc:acl-list" + + # rubocop:disable Metrics/AbcSize + def call(auth_strategy:, input_data:) + username = origin_user_id(auth_strategy:).value_or { return Failure(it) } + + permissions = parse_permission_mask(input_data.user_permissions) + + Authentication[auth_strategy].call(storage: @storage) do |http| + with_tagged_logger do + info "Getting the folder information" + Input::FileInfo.build(file_id: input_data.file_id).bind do |file_data| + Queries::FileInfoQuery.call(storage: @storage, auth_strategy:, input_data: file_data).bind do |folder_info| + info "Setting permissions #{permissions.inspect} on #{folder_info.location}" + body = request_xml_body(permissions[:groups], permissions[:users]) + # This can raise KeyErrors, we probably should just default to empty Arrays. + response = http.request("PROPPATCH", + UrlBuilder.url(@storage.uri, + "remote.php/dav/files", + username, + CGI.unescape(folder_info.location)), + xml: body) + + handle_response(response) + end + end + end + end + end + + # rubocop:enable Metrics/AbcSize + + private + + def parse_permission_mask(user_permissions) + user_permissions.each_with_object({ groups: {}, users: {} }) do |entry, aggregate| + if entry.key?(:user_id) + aggregate[:users][entry[:user_id]] = + PERMISSIONS_MAP.values_at(*(PERMISSIONS_KEYS & entry[:permissions])).sum + else + aggregate[:groups][entry[:group_id]] = + PERMISSIONS_MAP.values_at(*(PERMISSIONS_KEYS & entry[:permissions])).sum + end + end + end + + # rubocop:disable Metrics/AbcSize + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + doc = Nokogiri::XML(response.body.to_s) + if doc.xpath(SUCCESS_XPATH).present? + info "Permissions set" + Success(:success) + else + Failure(error.with(code: :permission_not_set)) + end + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + def request_xml_body(groups_permissions, users_permissions) + Nokogiri::XML::Builder.new do |xml| + xml["d"].propertyupdate( + "xmlns:d" => "DAV:", + "xmlns:nc" => "http://nextcloud.org/ns" + ) do + xml["d"].set do + xml["d"].prop do + xml["nc"].send(:"acl-list") do + groups_permissions.each do |group, group_permissions| + xml["nc"].acl do + xml["nc"].send(:"acl-mapping-type", "group") + xml["nc"].send(:"acl-mapping-id", group) + xml["nc"].send(:"acl-mask", "31") + xml["nc"].send(:"acl-permissions", group_permissions.to_s) + end + end + users_permissions.each do |user, user_permissions| + xml["nc"].acl do + xml["nc"].send(:"acl-mapping-type", "user") + xml["nc"].send(:"acl-mapping-id", user) + xml["nc"].send(:"acl-mask", "31") + xml["nc"].send(:"acl-permissions", user_permissions.to_s) + end + end + end + end + end + end + end.to_xml + end + + # rubocop:enable Metrics/AbcSize + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/audience_contract.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/audience_contract.rb new file mode 100644 index 00000000000..618ae67e26d --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/audience_contract.rb @@ -0,0 +1,57 @@ +# 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 Adapters + module Providers + module Nextcloud + module Contracts + class AudienceContract < ::ModelContract + attribute :storage_audience + validates :storage_audience, presence: true, if: -> { nextcloud_storage_authenticate_via_idp? } + + # Adding this to allow writing the storage_audience + attribute :provider_fields + + private + + def nextcloud_storage_authenticate_via_idp? + nextcloud_storage? && @model.authenticate_via_idp? + end + + def nextcloud_storage? + @model.is_a?(NextcloudStorage) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/automatic_management_contract.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/automatic_management_contract.rb new file mode 100644 index 00000000000..bddef124450 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/automatic_management_contract.rb @@ -0,0 +1,78 @@ +# 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 Adapters + module Providers + module Nextcloud + module Contracts + class AutomaticManagementContract < ::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? + # FIXME: Mode this to Adapters - 2025-03-18 @mereghost + 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?(NextcloudStorage) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/general_information_contract.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/general_information_contract.rb new file mode 100644 index 00000000000..1687288dd34 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/general_information_contract.rb @@ -0,0 +1,63 @@ +# 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 Adapters + module Providers + module Nextcloud + module Contracts + class GeneralInformationContract < ::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: NextcloudStorage::AUTHENTICATION_METHODS } + + validate :require_ee_token_for_sso + + def require_ee_token_for_sso + return if EnterpriseToken.allows_to?(:nextcloud_sso) + return unless model.authenticate_via_idp? + return unless model.authentication_method_changed? + + plan_name = I18n.t("ee.upsell.plan_name", plan: OpenProject::Token.lowest_plan_for(:nextcloud_sso)&.capitalize) + errors.add(:authentication_method, :enterprise_plan_required, plan_name:) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/storage_contract.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/storage_contract.rb new file mode 100644 index 00000000000..b84a12647a8 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/contracts/storage_contract.rb @@ -0,0 +1,45 @@ +# 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 Adapters + module Providers + module Nextcloud + module Contracts + class StorageContract < ComposedContract + include_contract GeneralInformationContract + include_contract AudienceContract + include_contract AutomaticManagementContract + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/oauth_configurations/configuration_interface.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/managed_folder_identifier.rb similarity index 72% rename from modules/storages/app/common/storages/peripherals/oauth_configurations/configuration_interface.rb rename to modules/storages/app/common/storages/adapters/providers/nextcloud/managed_folder_identifier.rb index 463d1824e06..6f57eb1e433 100644 --- a/modules/storages/app/common/storages/peripherals/oauth_configurations/configuration_interface.rb +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/managed_folder_identifier.rb @@ -29,19 +29,26 @@ #++ module Storages - module Peripherals - module OAuthConfigurations - class ConfigurationInterface - using ServiceResultRefinements + module Adapters + module Providers + module Nextcloud + class ManagedFolderIdentifier + def initialize(project_storage) + @storage = project_storage.storage + @project = project_storage.project + end - def scope = raise ::Storages::Errors::SubclassResponsibility + def name + "#{@project.name.tr('/', '|')} (#{@project.id})" + end - def basic_rack_oauth_client = raise ::Storages::Errors::SubclassResponsibility + def path + "/#{@storage.group_folder}/#{name}/" + end - def to_httpx_oauth_config = raise ::Storages::Errors::SubclassResponsibility - - def authorization_uri(state: nil) - basic_rack_oauth_client.authorization_uri(scope:, state:) + def location + path + end end end end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/nextcloud_registry.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/nextcloud_registry.rb new file mode 100644 index 00000000000..8f96b548c7c --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/nextcloud_registry.rb @@ -0,0 +1,106 @@ +# 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 Adapters + module Providers + module Nextcloud + NextcloudRegistry = Dry::Container::Namespace.new("nextcloud") do + namespace("authentication") do + register(:userless, ->(*) { Input::Strategy.build(key: :basic_auth) }) + register(:user_bound, UserBoundAuthentication) + end + + namespace("commands") do + register(:add_user_to_group, Commands::AddUserToGroupCommand) + register(:copy_template_folder, Commands::CopyTemplateFolderCommand) + register(:create_folder, Commands::CreateFolderCommand) + register(:delete_folder, Commands::DeleteFolderCommand) + register(:remove_user_from_group, Commands::RemoveUserFromGroupCommand) + register(:rename_file, Commands::RenameFileCommand) + register(:set_permissions, Commands::SetPermissionsCommand) + end + + namespace("components") do + namespace("forms") do + register(:automatically_managed_folders, Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent) + register(:general_information, Admin::Forms::GeneralInfoFormComponent) + register(:storage_audience, Admin::Forms::StorageAudienceFormComponent) + register(:oauth_application, Admin::OAuthApplicationInfoCopyComponent) + register(:oauth_client, Admin::Forms::OAuthClientFormComponent) + end + + register(:setup_wizard, StorageWizard) + + register(:automatically_managed_folders, Admin::AutomaticallyManagedProjectFoldersInfoComponent) + register(:general_information, Admin::GeneralInfoComponent) + register(:storage_audience, Admin::StorageAudienceInfoComponent) + register(:oauth_application, Admin::OAuthApplicationInfoComponent) + register(:oauth_client, Admin::OAuthClientInfoComponent) + end + + namespace("contracts") do + register(:storage, Contracts::StorageContract) + register(:general_information, Contracts::GeneralInformationContract) + register(:storage_audience, Contracts::AudienceContract) + end + + namespace("models") do + register(:managed_folder_identifier, ManagedFolderIdentifier) + end + + namespace("queries") do + register(:capabilities, Queries::CapabilitiesQuery) + register(:download_link, Queries::DownloadLinkQuery) + register(:file_info, Queries::FileInfoQuery) + register(:file_path_to_id_map, Queries::FilePathToIdMapQuery) + register(:files, Queries::FilesQuery) + register(:files_info, Queries::FilesInfoQuery) + register(:group_users, Queries::GroupUsersQuery) + register(:open_file_link, Queries::OpenFileLinkQuery) + register(:open_storage, Queries::OpenStorageQuery) + register(:upload_link, Queries::UploadLinkQuery) + register(:user, Queries::UserQuery) + end + + # Move Services to the Providers folder. + namespace("services") do + register(:upkeep_managed_folders, NextcloudManagedFolderCreateService) + register(:upkeep_managed_folder_permissions, NextcloudManagedFolderPermissionsService) + end + + namespace("validators") do + register("connection", Validators::ConnectionValidator) + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_path_to_id_map_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/oauth_configuration.rb similarity index 52% rename from modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_path_to_id_map_query.rb rename to modules/storages/app/common/storages/adapters/providers/nextcloud/oauth_configuration.rb index 338f0aeac99..2ea1a1229ba 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_path_to_id_map_query.rb +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/oauth_configuration.rb @@ -29,40 +29,46 @@ #++ module Storages - module Peripherals - module StorageInteraction + module Adapters + module Providers module Nextcloud - class FilePathToIdMapQuery - def self.call(storage:, auth_strategy:, folder:, depth: Float::INFINITY) - new(storage).call(auth_strategy:, folder:, depth:) - end + class OAuthConfiguration < OAuthConfigurationBase + attr_reader :oauth_client def initialize(storage) + super() @storage = storage - @propfind_query = Internal::PropfindQuery.new(storage) + raise(ArgumentError, "Storage must have configured OAuth client credentials") if storage.oauth_client.blank? + + @oauth_client = storage.oauth_client.freeze end - def call(auth_strategy:, folder:, depth:) - origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) - .on_failure { return it } - .result - - Authentication[auth_strategy].call(storage: @storage, http_options: headers(depth)) do |http| - # nc:acl-list is only required to avoid https://community.openproject.org/wp/49628. See comment #4. - @propfind_query.call(http:, - username: origin_user_id, - path: folder.path, - props: %w[oc:fileid nc:acl-list]) - .map do |obj| - obj.transform_values { |value| StorageFileId.new(id: value["fileid"]) } - end - end + def to_httpx_oauth_config + AuthenticationStrategies::OAuthConfiguration.new( + client_id: @oauth_client.client_id, + client_secret: @oauth_client.client_secret, + issuer: URI(UrlBuilder.url(@storage.uri, "/index.php/apps/oauth2/api/v1")).normalize, + scope: [] + ) end - private + def scope + [] + end - def headers(depth) - Util.webdav_request_with_depth(depth.to_s.downcase) + def basic_rack_oauth_client + uri = @storage.uri + + Rack::OAuth2::Client.new( + identifier: @oauth_client.client_id, + secret: @oauth_client.client_secret, + redirect_uri: @oauth_client.redirect_uri, + scheme: uri.scheme, + host: uri.host, + port: uri.port, + authorization_endpoint: UrlBuilder.path(uri.path, "/index.php/apps/oauth2/authorize"), + token_endpoint: UrlBuilder.path(uri.path, "/index.php/apps/oauth2/api/v1/token") + ) end end end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/provider_results/capabilities.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/provider_results/capabilities.rb new file mode 100644 index 00000000000..0daf3813d8a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/provider_results/capabilities.rb @@ -0,0 +1,64 @@ +# 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 Adapters + module Providers + module Nextcloud + module ProviderResults + Capabilities = Data.define(:app_enabled, :group_folder_enabled, :app_version, :group_folder_version) do + private_class_method :new + + def self.build(app_enabled:, group_folder_enabled:, app_version:, + group_folder_version:, contract: CapabilitiesContract.new) + contract.call(app_enabled:, group_folder_enabled:, app_version:, group_folder_version:) + .to_monad.fmap { new(**it.to_h) } + end + + def self.empty + new(app_enabled: false, group_folder_enabled: false, app_version: nil, group_folder_version: nil) + end + + alias_method :app_enabled?, :app_enabled + alias_method :group_folder_enabled?, :group_folder_enabled + + def app_disabled? = !app_enabled? + def group_folder_disabled? = !group_folder_enabled? + + def with(...) + args = to_h.merge(...) + self.class.build(**args).either(-> { it }, -> { raise ArgumentError, it.errors }) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/provider_results/capabilities_contract.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/provider_results/capabilities_contract.rb new file mode 100644 index 00000000000..7538503b3dd --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/provider_results/capabilities_contract.rb @@ -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 + module Adapters + module Providers + module Nextcloud + module ProviderResults + class CapabilitiesContract < Dry::Validation::Contract + params do + required(:app_enabled).filled(:bool) + required(:group_folder_enabled).filled(:bool) + required(:app_version).maybe(AdapterTypes::SemanticVersionType) + required(:group_folder_version).maybe(AdapterTypes::SemanticVersionType) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/capabilities_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/capabilities_query.rb new file mode 100644 index 00000000000..fd47a5a1ef6 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/capabilities_query.rb @@ -0,0 +1,90 @@ +# 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 Adapters + module Providers + module Nextcloud + module Queries + class CapabilitiesQuery < Base + def self.call(storage:, auth_strategy:) + new(storage).call(auth_strategy:) + end + + def call(auth_strategy:) + http_options = { headers: { Accept: "application/json" } }.deep_merge(ocs_api_request_headers) + Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| + handle_response(http.get(url)) + end + end + + private + + def url = UrlBuilder.url(@storage.uri, "/ocs/v2.php/cloud/capabilities") + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + json = response.json(symbolize_keys: true) + parse_capabilities(json) + in { status: 404 } + Failure(error.with(code: :not_found)) + else + Failure(error.with(code: :error)) + end + end + + def parse_capabilities(json) + app_json = json.dig(:ocs, :data, :capabilities, :integration_openproject) + + ProviderResults::Capabilities.build( + app_enabled: app_json.present?, + app_version: version(app_json&.dig(:app_version)), + group_folder_enabled: !!app_json&.dig(:groupfolders_enabled), + group_folder_version: version(app_json&.dig(:groupfolder_version)) + ) + end + + def version(str) + return if str.nil? + + major, minor, patch = str.split(".").map(&:to_i) + return if major.nil? || minor.nil? || patch.nil? + + SemanticVersion.new(major:, minor:, patch:) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/download_link_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/download_link_query.rb new file mode 100644 index 00000000000..cb3c40ac760 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/download_link_query.rb @@ -0,0 +1,98 @@ +# 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 Adapters + module Providers + module Nextcloud + module Queries + class DownloadLinkQuery < Base + def call(auth_strategy:, input_data:) + Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| + handle_response(http.post(request_url, json: { fileId: input_data.file_link.origin_id })).fmap do |token| + URI(download_link(token, input_data.file_link.origin_name)) + end + end + + # direct_download_request(auth_strategy:, file_link: input_data.file_link) + # .bind { |response_body| direct_download_token(body: response_body) } + # .map { |download_token| download_link(download_token, file_link.origin_name) } + end + + private + + def request_url = UrlBuilder.url(@storage.uri, "/ocs/v2.php/apps/dav/api/v1/direct") + + def http_options = { headers: { "Accept" => "application/json" } }.deep_merge(ocs_api_request_headers) + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + if response.body.blank? + Failure(error.with(code: :unauthorized)) + else + build_download_link(response, error) + end + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: error)) + end + end + + def build_download_link(response, error) + parsing_error = Failure(error.with(code: :invalid_response, payload: response.body)) + + json = response.json(symbolize_keys: true) + url = json.dig(:ocs, :data, :url) + return parsing_error if url.blank? + + path = URI.parse(url).path + return parsing_error if path.blank? + + token = path.split("/").last + return parsing_error if token.blank? + + Success(token) + end + + def download_link(token, origin_name) + UrlBuilder.url(@storage.uri, "index.php/apps/integration_openproject/direct", token, origin_name) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/file_info_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/file_info_query.rb new file mode 100644 index 00000000000..e6b8427e29e --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/file_info_query.rb @@ -0,0 +1,117 @@ +# 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 Adapters + module Providers + module Nextcloud + module Queries + class FileInfoQuery < Base + FILE_INFO_PATH = "ocs/v1.php/apps/integration_openproject/fileinfo" + + def call(auth_strategy:, input_data:) + http_options = ocs_api_request_headers.deep_merge(headers: { "Accept" => "application/json" }) + Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| + file_info(http, input_data.file_id).bind do |json| + validate_response_object(json).bind do |valid_response| + create_storage_file_info(valid_response) + end + end + end + end + + private + + def file_info(http, file_id) + response = http.get(UrlBuilder.url(@storage.uri, FILE_INFO_PATH, file_id)) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + Success(response.json(symbolize_keys: true)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + def validate_response_object(json) + error = Results::Error.new(source: self.class, payload: json) + + case json.dig(:ocs, :data, :statuscode) + when 200..299 + Success(json) + when 403 + Failure(error.with(code: :forbidden)) + when 404 + Failure(error.with(code: :not_found)) + else + Failure(error.with(code: :error)) + end + end + + def create_storage_file_info(json) # rubocop:disable Metrics/AbcSize + data = json.dig(:ocs, :data) + Results::StorageFileInfo.build( + status: data[:status]&.downcase, + status_code: data[:statuscode], + id: data[:id]&.to_s, + name: data[:name], + last_modified_at: Time.zone.at(data[:mtime]).iso8601, + created_at: Time.zone.at(data[:ctime]).iso8601, + mime_type: data[:mimetype], + size: data[:size], + owner_name: data[:owner_name], + owner_id: data[:owner_id], + last_modified_by_name: data[:modifier_name], + last_modified_by_id: data[:modifier_id], + permissions: data[:dav_permissions], + location: location(data[:path]) + ) + end + + def location(file_path) + prefix = "files/" + idx = file_path.index(prefix) + return "/" if idx == nil + + idx += prefix.length - 1 + + UrlBuilder.path(file_path[idx..]) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/file_path_to_id_map_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/file_path_to_id_map_query.rb new file mode 100644 index 00000000000..2547f593064 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/file_path_to_id_map_query.rb @@ -0,0 +1,67 @@ +# 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 Adapters + module Providers + module Nextcloud + module Queries + class FilePathToIdMapQuery < Base + def initialize(*) + super + @propfind_query = PropfindQuery.new(@storage) + end + + def call(auth_strategy:, input_data:) + origin_user_id(auth_strategy:).bind do |origin_user_id| + Authentication[auth_strategy].call(storage: @storage, http_options: headers(input_data.depth)) do |http| + # nc:acl-list is only required to avoid https://community.openproject.org/wp/49628. See comment #4. + @propfind_query.call(http:, + username: origin_user_id, + path: input_data.folder.path, + props: %w[oc:fileid nc:acl-list]) + .fmap do |obj| + obj.transform_values { |value| StorageFileId.new(id: value["fileid"]) } + end + end + end + end + + private + + def headers(depth) + { headers: { "Depth" => depth.to_s.downcase } } + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/files_info_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/files_info_query.rb new file mode 100644 index 00000000000..e462e2c8f40 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/files_info_query.rb @@ -0,0 +1,130 @@ +# 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 Adapters + module Providers + module Nextcloud + module Queries + class FilesInfoQuery < Base + FILES_INFO_PATH = "ocs/v1.php/apps/integration_openproject/filesinfo" + + def call(auth_strategy:, input_data:) + return Success([]) if input_data.file_ids.empty? + + with_tagged_logger do + info "Retrieving file information for #{input_data.file_ids.join(', ')}" + + http_options = ocs_api_request_headers.deep_merge(headers: { "Accept" => "application/json" }) + Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| + files_info(http, input_data.file_ids).fmap { create_storage_file_infos(it) } + end + end + end + + private + + def files_info(http, file_ids) + response = http.post(UrlBuilder.url(@storage.uri, FILES_INFO_PATH), json: { fileIds: file_ids }) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + verify_successful_response(response.json(symbolize_keys: true), error) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + def verify_successful_response(json, error) + if json.dig(:ocs, :meta, :status) == "ok" + Success(json) + else + Failure(error.with(code: :error)) + end + end + + def create_storage_file_infos(parsed_json) + parsed_json.dig(:ocs, :data)&.map do |(key, value)| + if value[:statuscode] == 200 + build_file_info(value).bind { it } + else + Results::StorageFileInfo.new( + status: value[:status], + status_code: value[:statuscode], + id: key.to_s + ) + end + end + end + + # rubocop:disable Metrics/AbcSize + def build_file_info(value) + Results::StorageFileInfo.build( + status: value[:status], + status_code: value[:statuscode], + id: value[:id].to_s, + name: value[:name], + last_modified_at: Time.zone.at(value[:mtime]), + created_at: Time.zone.at(value[:ctime]), + mime_type: value[:mimetype], + size: value[:size], + owner_name: value[:owner_name], + owner_id: value[:owner_id], + last_modified_by_name: value[:modifier_name], + last_modified_by_id: value[:modifier_id], + permissions: value[:dav_permissions], + location: location(value[:path], value[:mimetype]) + ) + end + # rubocop:enable Metrics/AbcSize + + def location(file_path, mimetype) + prefix = "files/" + idx = file_path.index(prefix) + return "/" if idx == nil + + idx += prefix.length - 1 + # Remove the following when /filesinfo starts responding with a trailing slash for directory paths + # in all supported versions of OpenProjectIntegation Nextcloud App. + file_path << "/" if mimetype == "application/x-op-directory" && file_path[-1] != "/" + + UrlBuilder.path(file_path[idx..]) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/files_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/files_query.rb new file mode 100644 index 00000000000..43d503c4822 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/files_query.rb @@ -0,0 +1,211 @@ +# 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 Adapters + module Providers + module Nextcloud + module Queries + class FilesQuery < Base + def call(auth_strategy:, input_data:) + origin_user_id(auth_strategy:).bind do |origin_user| + @location_prefix = CGI.unescape(UrlBuilder.path(@storage.uri.path, "remote.php/dav/files", origin_user)) + make_request(auth_strategy:, folder: input_data.folder, origin_user:).bind do |xml| + storage_files(xml) + end + end + end + + private + + def make_request(auth_strategy:, folder:, origin_user:) + Authentication[auth_strategy].call(storage: @storage, http_options: depth_header(1)) do |http| + response = http.request("PROPFIND", + UrlBuilder.url(@storage.uri, + "remote.php/dav/files", + origin_user, + folder.path), + xml: requested_properties) + handle_response(response) + end + end + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + Success(response.xml) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + # rubocop:disable Metrics/AbcSize + 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"].getcontenttype + xml["d"].getlastmodified + xml["oc"].permissions + xml["oc"].send(:"owner-display-name") + end + end + end.to_xml + end + # rubocop:enable Metrics/AbcSize + + def storage_files(xml) + parent, *files = xml.xpath("//d:response").to_a.map { |file_element| storage_file(file_element) } + + Results::StorageFileCollection.build(files:, parent:, ancestors: ancestors(parent.location)) + end + + def ancestors(parent_location) + path = parent_location.split("/") + return [] if path.none? + + path.take(path.count - 1).reduce([]) do |list, item| + last = list.last + prefix = last.nil? || last.location[-1] != "/" ? "/" : "" + location = "#{last&.location}#{prefix}#{item}" + list.append(forge_ancestor(location)) + end + end + + # The ancestors are simply derived objects from the parents location string. Until we have real information + # from the nextcloud API about the path to the parent, we need to derive name, location and forge an ID. + def forge_ancestor(location) + Results::StorageFile.new(id: Digest::SHA256.hexdigest(location), name: name(location), location:) + end + + def name(location) + location == "/" ? "Root" : CGI.unescape(location.split("/").last) + end + + def storage_file(file_element) + location = location(file_element) + + Results::StorageFile.new( + id: id(file_element), + name: name(location), + size: size(file_element), + mime_type: mime_type(file_element), + last_modified_at: last_modified_at(file_element), + created_by_name: created_by(file_element), + location:, + permissions: permissions(file_element) + ) + end + + def id(element) + element + .xpath(".//oc:fileid") + .map(&:inner_text) + .reject(&:empty?) + .first + end + + def location(element) + texts = element + .xpath("d:href") + .map(&:inner_text) + + return nil if texts.empty? + + element_name = texts.first.delete_prefix(@location_prefix) + + return element_name if element_name == "/" + + element_name.delete_suffix("/") + end + + def size(element) + element + .xpath(".//oc:size") + .map(&:inner_text) + .map { |e| Integer(e) } + .first + end + + def mime_type(element) + element + .xpath(".//d:getcontenttype") + .map(&:inner_text) + .reject(&:empty?) + .first || "application/x-op-directory" + end + + def last_modified_at(element) + element + .xpath(".//d:getlastmodified") + .map { |e| DateTime.parse(e) } + .first + end + + def created_by(element) + element + .xpath(".//oc:owner-display-name") + .map(&:inner_text) + .reject(&:empty?) + .first + end + + def permissions(element) + permissions_string = + element + .xpath(".//oc:permissions") + .map(&:inner_text) + .reject(&:empty?) + .first + + # Nextcloud Dav permissions: + # https://github.com/nextcloud/server/blob/66648011c6bc278ace57230db44fd6d63d67b864/lib/public/Files/DavUtil.php + result = [] + result << :readable if permissions_string&.include?("G") + result << :writeable if permissions_string&.match?(/W|CK/) + result + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/group_users_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/group_users_query.rb new file mode 100644 index 00000000000..720a5868030 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/group_users_query.rb @@ -0,0 +1,91 @@ +# 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 Adapters + module Providers + module Nextcloud + module Queries + class GroupUsersQuery < Base + include TaggedLogging + + def call(auth_strategy:, input_data:) + with_tagged_logger do + Authentication[auth_strategy].call(storage: @storage, http_options: ocs_api_request_headers) do |http| + url = UrlBuilder.url(@storage.uri, "ocs/v1.php/cloud/groups", input_data.group) + info "Requesting user list for group #{input_data.group} via url #{url} " + + handle_response(http.get(url)) + end + end + end + + private + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + handle_success_response(response, error) + in { status: 405 } + Failure(error.with(code: :not_allowed)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 409 } + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :error)) + end + end + + def handle_success_response(response, error) + xml = Nokogiri::XML(response.body.to_s) + status_code = xml.xpath("/ocs/meta/statuscode").text + + case status_code + when "100" + group_users = xml.xpath("/ocs/data/users/element").map(&:text) + info "#{group_users.size} users found" + Success(group_users) + when "404" + Failure(error.with(code: :group_does_not_exist)) + else + Failure(error.with(code: :error)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/upload_data.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/open_file_link_query.rb similarity index 74% rename from modules/storages/app/common/storages/peripherals/storage_interaction/inputs/upload_data.rb rename to modules/storages/app/common/storages/adapters/providers/nextcloud/queries/open_file_link_query.rb index 80ed8c417e0..4592e549d64 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/upload_data.rb +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/open_file_link_query.rb @@ -29,16 +29,16 @@ #++ module Storages - module Peripherals - module StorageInteraction - module Inputs - UploadData = Data.define(:folder_id, :file_name) do - private_class_method :new - - def self.build(folder_id:, file_name:, contract: UploadDataContract.new) - contract.call(folder_id:, file_name:) - .to_monad - .fmap { |result| new(file_name: result[:file_name], folder_id: result[:folder_id]) } + module Adapters + module Providers + module Nextcloud + module Queries + class OpenFileLinkQuery < Base + def call(input_data:, **) + location_flag = input_data.open_location ? 0 : 1 + url = UrlBuilder.url(@storage.uri, "index.php/f/#{input_data.file_id}") + "?openfile=#{location_flag}" + Success(url) + end end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/upload_data_contract.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/open_storage_query.rb similarity index 83% rename from modules/storages/app/common/storages/peripherals/storage_interaction/inputs/upload_data_contract.rb rename to modules/storages/app/common/storages/adapters/providers/nextcloud/queries/open_storage_query.rb index 6326ce94cbd..2186fb1289e 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/upload_data_contract.rb +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/open_storage_query.rb @@ -29,13 +29,14 @@ #++ module Storages - module Peripherals - module StorageInteraction - module Inputs - class UploadDataContract < Dry::Validation::Contract - params do - required(:folder_id).filled(:string) - required(:file_name).filled(:string) + module Adapters + module Providers + module Nextcloud + module Queries + class OpenStorageQuery < Base + def call(**) + Success(UrlBuilder.url(@storage.uri, "index.php/apps/files")) + end end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/propfind_query.rb similarity index 81% rename from modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb rename to modules/storages/app/common/storages/adapters/providers/nextcloud/queries/propfind_query.rb index e0c31a4bfcc..d9244fd1b97 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/propfind_query.rb +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/propfind_query.rb @@ -29,11 +29,11 @@ #++ module Storages - module Peripherals - module StorageInteraction + module Adapters + module Providers module Nextcloud - module Internal - class PropfindQuery + module Queries + class PropfindQuery < Base # Only for information purposes currently. # Probably a bit later we could validate `#call` parameters. # @@ -71,10 +71,6 @@ module Storages new(storage).call(http:, username:, path:, props:) end - def initialize(storage) - @storage = storage - end - def call(http:, username:, path:, props:) request_uri = UrlBuilder.url(@storage.uri, "remote.php/dav/files", username, path) response = http.request(:propfind, request_uri, xml: request_body(props)) @@ -85,26 +81,19 @@ module Storages private def handle_response(response, username) + error = Results::Error.new(source: self.class, payload: response) + case response in { status: 200..299 } success_result(response, username) in { status: 401 } - Util.failure(code: :unauthorized, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request not authorized!") + Failure(error.with(code: :unauthorized)) in { status: 404 } - Util.failure(code: :not_found, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request destination not found!") + Failure(error.with(code: :not_found)) in { status: 405 } - Util.failure(code: :not_allowed, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request method not allowed!") - + Failure(error.with(code: :not_allowed)) else - Util.failure(code: :error, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request failed with unknown error!") + Failure(error.with(code: :error)) end end @@ -123,7 +112,7 @@ module Storages end end - ServiceResult.success(result:) + Success(result) end # rubocop:enable Metrics/AbcSize diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/upload_link_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/upload_link_query.rb new file mode 100644 index 00000000000..39ff19f809c --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/upload_link_query.rb @@ -0,0 +1,86 @@ +# 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 Adapters + module Providers + module Nextcloud + module Queries + class UploadLinkQuery < Base + def call(auth_strategy:, input_data:) + with_tagged_logger do + Authentication[auth_strategy].call(storage: @storage) do |http| + response = http.post(base_uri, json: payload_from(input_data)) + + handle_response(response).bind do |rsp| + Results::UploadLink.build(destination: "#{upload_base_uri}/#{rsp[:token]}", method: :post) + end + end + end + end + + private + + def base_uri + UrlBuilder.url(@storage.uri, "index.php/apps/integration_openproject/direct-upload-token") + end + + def upload_base_uri + UrlBuilder.url(@storage.uri, "index.php/apps/integration_openproject/direct-upload") + end + + def payload_from(upload_data) + { folder_id: upload_data.folder_id } + end + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + info "Upload link generated successfully." + Success(response.json(symbolize_keys: true)) + in { status: 404 } + info "The parent folder was not found." + Failure(error.with(code: :not_found)) + in { status: 401 } + info "User authorization failed." + Failure(error.with(code: :unauthorized)) + else + info "Unknown error happened." + Failure(error.with(code: :error)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/delete_entity_command.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/user_query.rb similarity index 52% rename from modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/delete_entity_command.rb rename to modules/storages/app/common/storages/adapters/providers/nextcloud/queries/user_query.rb index f2de1f6a310..6e7ea2c3d77 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/internal/delete_entity_command.rb +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/queries/user_query.rb @@ -29,49 +29,45 @@ #++ module Storages - module Peripherals - module StorageInteraction + module Adapters + module Providers module Nextcloud - module Internal - class DeleteEntityCommand - def self.call(storage:, auth_strategy:, location:) - new(storage).call(auth_strategy:, location:) + module Queries + class UserQuery < Base + def self.call(storage:, auth_strategy:) + new(storage).call(auth_strategy:) end - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, location:) - origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) - .on_failure { |error| return error } - .result - - Authentication[auth_strategy].call(storage: @storage) do |http| - handle_response http.delete( - UrlBuilder.url(@storage.uri, "remote.php/dav/files", origin_user_id, location) - ) + def call(auth_strategy:) + Authentication[auth_strategy].call(storage: @storage, http_options: ocs_api_request_headers) do |http| + handle_response(http.get(UrlBuilder.url(@storage.uri, "/ocs/v1.php/cloud/user"))) end end private def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) case response in { status: 200..299 } - ServiceResult.success - in { status: 404 } - Util.failure(code: :not_found, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request destination not found!") + handle_success_response(response) in { status: 401 } - Util.failure(code: :unauthorized, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request not authorized!") + Failure(error.with(code: :unauthorized)) else - Util.failure(code: :error, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request failed with unknown error!") + Failure(error.with(code: :error)) + end + end + + def handle_success_response(response) + error = Results::Error.new(source: self.class, payload: response) + xml = Nokogiri::XML(response.body.to_s) + statuscode = xml.xpath("/ocs/meta/statuscode").text + + case statuscode + when "100" + Success({ id: xml.xpath("/ocs/data/id").text }) + else + Failure(error.with(code: :error)) end end end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/storage_wizard.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/storage_wizard.rb new file mode 100644 index 00000000000..a5241fdfed4 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/storage_wizard.rb @@ -0,0 +1,79 @@ +# 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 Adapters + module Providers + module Nextcloud + class StorageWizard < Wizard + step :general_information, completed_if: ->(storage) { storage.host.present? && storage.name.present? } + + # OAuth 2.0 SSO + + step :storage_audience, + section: :oauth_configuration, + if: ->(storage) { storage.authenticate_via_idp? }, + completed_if: ->(storage) { storage.storage_audience.present? } + + # Two-Way OAuth 2.0 + + step :oauth_application, + section: :oauth_configuration, + if: ->(storage) { storage.authenticate_via_storage? }, + completed_if: ->(storage) { storage.oauth_application.present? }, + preparation: :prepare_oauth_application + + step :oauth_client, + section: :oauth_configuration, + if: ->(storage) { storage.authenticate_via_storage? }, + completed_if: ->(storage) { storage.oauth_client.present? }, + preparation: ->(storage) { storage.build_oauth_client } + + step :automatically_managed_folders, + completed_if: ->(storage) { !storage.automatic_management_unspecified? }, + preparation: :prepare_storage_for_automatic_management_form + + private + + def prepare_oauth_application(storage) + create_result = ::Storages::OAuthApplications::CreateService.new(storage:, user:).call + storage.oauth_application = create_result.result if create_result.success? + end + + def prepare_storage_for_automatic_management_form(storage) + ::Storages::Storages::SetProviderFieldsAttributesService + .new(user:, model: storage, contract_class: EmptyContract) + .call + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/open_storage_query.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/user_bound_authentication.rb similarity index 74% rename from modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/open_storage_query.rb rename to modules/storages/app/common/storages/adapters/providers/nextcloud/user_bound_authentication.rb index 57cdeaf94d1..b70f006b7f4 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/open_storage_query.rb +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/user_bound_authentication.rb @@ -29,24 +29,28 @@ #++ module Storages - module Peripherals - module StorageInteraction + module Adapters + module Providers module Nextcloud - class OpenStorageQuery - def self.call(storage:, auth_strategy:) - new(storage).call(auth_strategy:) + class UserBoundAuthentication + def self.call(user, storage) + new(user, storage).call end - def initialize(storage) + def initialize(user, storage) + @user = user @storage = storage end - # rubocop:disable Lint/UnusedMethodArgument - def call(auth_strategy:) - ServiceResult.success(result: UrlBuilder.url(@storage.uri, "index.php/apps/files")) - end + def call + key = if @storage.authenticate_via_idp? && @user.provided_by_oidc? + :sso_user_token + else + :oauth_user_token + end - # rubocop:enable Lint/UnusedMethodArgument + Input::Strategy.build(key:, user: @user) + end end end end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/ampf_configuration_validator.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/ampf_configuration_validator.rb new file mode 100644 index 00000000000..037c617bc06 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/ampf_configuration_validator.rb @@ -0,0 +1,136 @@ +# 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 Adapters + module Providers + module Nextcloud + module Validators + class AmpfConfigurationValidator < ConnectionValidators::BaseValidatorGroup + def self.key = :ampf_configuration + + private + + def validate + register_checks( + :group_folder_app, :files_request, :userless_access, :group_folder_presence, :group_folder_contents + ) + + group_folder_app_checks + files_request_failed_with_unknown_error + userless_access_denied + group_folder_not_found + with_unexpected_content + end + + def userless_access_denied + files.or { it.code == :unauthorized and fail_check(:userless_access, :nc_userless_access_denied) } + pass_check(:userless_access) + end + + def group_folder_app_checks + required_version = SemanticVersion.parse( + nextcloud_dependencies.dig("dependencies", "group_folders_app", "min_version") + ) + + capabilities = Registry["nextcloud.queries.capabilities"].call(storage: @storage, auth_strategy: noop).value! + dependency = I18n.t("storages.dependencies.nextcloud.group_folders_app") + + if capabilities.group_folder_disabled? + fail_check(:group_folder_app, :nc_dependency_missing, context: { dependency: }) + elsif capabilities.group_folder_version < required_version + fail_check(:group_folder_app, :nc_dependency_version_mismatch, context: { dependency: }) + else + pass_check(:group_folder_app) + end + end + + def group_folder_not_found + files.or { it.code == :not_found and fail_check(:group_folder_presence, :nc_group_folder_not_found) } + pass_check(:group_folder_presence) + end + + def files_request_failed_with_unknown_error + files.or do |failure| + if failure.code == :error + error "Connection validation failed with unknown error:\n" \ + "\tstorage: ##{@storage.id} #{@storage.name}\n" \ + "\trequest: Group folder content\n" \ + "\tstatus: #{failure}\n" \ + "\tresponse: #{failure.payload}" + + fail_check(:files_request, :unknown_error) + end + end + pass_check(:files_request) + end + + def with_unexpected_content + unexpected_files = files.value!.reject { managed_project_folder_ids.include?(it.id) } + return pass_check(:group_folder_contents) if unexpected_files.empty? + + log_extraneous_files(unexpected_files) + warn_check(:group_folder_contents, :nc_unexpected_content) + end + + def log_extraneous_files(unexpected_files) + file_representation = unexpected_files.map do |file| + "Name: #{file.name}, ID: #{file.id}, Location: #{file.location}" + end + + warn "Unexpected files/folder found in group folder:\n\t#{file_representation.join("\n\t")}" + end + + def auth_strategy = Registry["nextcloud.authentication.userless"].call + + def managed_project_folder_ids + @managed_project_folder_ids ||= ProjectStorage.automatic.where(storage: @storage) + .pluck(:project_folder_id).to_set + end + + def files + @files ||= Input::Files.build(folder: @storage.group_folder).bind do |input_data| + Registry.resolve("#{@storage}.queries.files").call(storage: @storage, auth_strategy:, input_data:) + end + end + + def noop = Input::Strategy.build(key: :noop) + + def nextcloud_dependencies + @nextcloud_dependencies ||= YAML.load_file(path_to_config).deep_stringify_keys + end + + def path_to_config = Rails.root.join("modules/storages/config/nextcloud_dependencies.yml") + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/authentication_validator.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/authentication_validator.rb new file mode 100644 index 00000000000..b845b2f9b1a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/authentication_validator.rb @@ -0,0 +1,140 @@ +# 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 Adapters + module Providers + module Nextcloud + module Validators + class AuthenticationValidator < ConnectionValidators::BaseValidatorGroup + def self.key = :authentication + + def initialize(storage) + super + @user = User.current + end + + private + + def validate + @storage.authenticate_via_idp? ? validate_sso : validate_oauth + end + + def validate_oauth + register_checks(:existing_token, :user_bound_request) + + oauth_token + user_bound_request + end + + def oauth_token + if OAuthClientToken.for_user_and_client(@user, @storage.oauth_client).exists? + pass_check(:existing_token) + else + warn_check(:existing_token, :nc_oauth_token_missing, halt_validation: true) + end + end + + def user_bound_request + Registry["nextcloud.queries.user"] + .call(storage: @storage, auth_strategy:) + .or { fail_check(:user_bound_request, :"nc_oauth_request_#{it.code}") } + + pass_check(:user_bound_request) + end + + def auth_strategy = Registry["nextcloud.authentication.user_bound"].call(@user, @storage) + + def validate_sso + register_checks( + :non_provisioned_user, + :provisioned_user_provider, + :token_negotiable, + :user_bound_request, + :offline_access + ) + + non_provisioned_user + non_oidc_provisioned_user + token_negotiable + user_bound_request + offline_access + end + + def non_provisioned_user + if @user.identity_url.present? + pass_check(:non_provisioned_user) + else + warn_check(:non_provisioned_user, :oidc_non_provisioned_user, halt_validation: true) + end + end + + def non_oidc_provisioned_user + if @user.authentication_provider.is_a?(OpenIDConnect::Provider) + pass_check(:provisioned_user_provider) + else + warn_check(:provisioned_user_provider, :oidc_non_oidc_user, halt_validation: true) + end + end + + def token_negotiable + service = OpenIDConnect::UserTokens::FetchService.new(user: @user) + + result = service.access_token_for(audience: @storage.audience) + return pass_check(:token_negotiable) if result.success? + + error_code = + case result.failure + in { code: /token_exchange/ | :unable_to_exchange_token } + :oidc_token_exchange_failed + in { code: /token_refresh/ } + :oidc_token_refresh_failed + in { code: :no_token_for_audience } + :oidc_token_acquisition_failed + else + :unknown_error + end + + fail_check(:token_negotiable, error_code) + end + + def offline_access + if @user.authentication_provider.scopes.include?("offline_access") + pass_check(:offline_access) + else + warn_check(:offline_access, :offline_access_scope_missing) + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/connection_validator.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/connection_validator.rb new file mode 100644 index 00000000000..0fd1877a003 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/connection_validator.rb @@ -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 + module Adapters + module Providers + module Nextcloud + module Validators + class ConnectionValidator < ConnectionValidators::BaseConnectionValidator + register_group StorageConfigurationValidator + register_group AuthenticationValidator, precondition: ->(_, result) { result.group(:base_configuration).non_failure? } + register_group AmpfConfigurationValidator, + precondition: ->(storage, result) do + result.group(:base_configuration).non_failure? && storage.automatic_management_enabled? + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/storage_configuration_validator.rb b/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/storage_configuration_validator.rb new file mode 100644 index 00000000000..8dc8cefc34c --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/nextcloud/validators/storage_configuration_validator.rb @@ -0,0 +1,117 @@ +# 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 Adapters + module Providers + module Nextcloud + module Validators + class StorageConfigurationValidator < ConnectionValidators::BaseValidatorGroup + def self.key = :base_configuration + + private + + def validate + register_checks(:storage_configured, :host_url_accessible, :capabilities_request, + :dependencies_check, :dependencies_versions) + + storage_configuration_status + host_url_not_found + capabilities_request_status + missing_dependencies + version_mismatch + end + + def storage_configuration_status + if @storage.configured? + pass_check(:storage_configured) + else + fail_check(:storage_configured, :not_configured) + end + end + + def capabilities_request_status + if capabilities.failure? && capabilities.failure.code != :not_found + fail_check(:capabilities_request, :unknown_error) + else + pass_check(:capabilities_request) + end + end + + def version_mismatch + min_app_version = SemanticVersion.parse(nextcloud_dependencies.dig("dependencies", "integration_app", + "min_version")) + capabilities_result = capabilities.value! + dependency = I18n.t("storages.dependencies.nextcloud.integration_app") + + if capabilities_result.app_version < min_app_version + fail_check(:dependencies_versions, :nc_dependency_version_mismatch, context: { dependency: }) + else + pass_check(:dependencies_versions) + end + end + + def missing_dependencies + capabilities_result = capabilities.value! + dependency = I18n.t("storages.dependencies.nextcloud.integration_app") + + if capabilities_result.app_disabled? + fail_check(:dependencies_check, :nc_dependency_missing, context: { dependency: }) + else + pass_check(:dependencies_check) + end + end + + def host_url_not_found + if capabilities.failure? && capabilities.failure.code == :not_found + fail_check(:host_url_accessible, :nc_host_not_found) + else + pass_check(:host_url_accessible) + end + end + + def noop = Input::Strategy.build(key: :noop) + + def capabilities + @capabilities ||= Registry.resolve("#{@storage}.queries.capabilities") + .call(storage: @storage, auth_strategy: noop) + end + + def nextcloud_dependencies + @nextcloud_dependencies ||= YAML.load_file(path_to_config).deep_stringify_keys! + end + + def path_to_config = Rails.root.join("modules/storages/config/nextcloud_dependencies.yml") + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/user_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/base.rb similarity index 58% rename from modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/user_query.rb rename to modules/storages/app/common/storages/adapters/providers/one_drive/base.rb index 3996c20a643..78fa2624ddf 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/user_query.rb +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/base.rb @@ -29,38 +29,38 @@ #++ module Storages - module Peripherals - module StorageInteraction + module Adapters + module Providers module OneDrive - class UserQuery - def self.call(storage:, auth_strategy:) - new(storage).call(auth_strategy:) + class Base + include TaggedLogging + include Dry::Monads::Result(Results::Error) + + def self.call(storage:, auth_strategy:, input_data:) + new(storage).call(auth_strategy:, input_data:) end def initialize(storage) @storage = storage end - def call(auth_strategy:) - Authentication[auth_strategy].call(storage: @storage) do |http| - handle_response http.get(UrlBuilder.url(@storage.uri, "/v1.0/me")) - end - end - private - def handle_response(response) - case response - in { status: 200..299 } - ServiceResult.success(result: { id: response.json["id"] }) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, errors: StorageError.new(code: :unauthorized)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, errors: StorageError.new(code: :forbidden)) - else - data = StorageErrorData.new(source: self.class, payload: response) - ServiceResult.failure(result: :error, errors: StorageError.new(code: :error, data:)) - end + # @param error [Results::Error] + def log_storage_error(error, context = {}) + data = case error.payload + in { status: Integer } + { status: error.payload&.status, body: error.payload&.body.to_s } + else + error.payload.to_s + end + + error_message = context.merge({ error_code: error.code, data: }) + error error_message + end + + def base_uri + URI.parse(UrlBuilder.url(@storage.uri, "/v1.0/drives", @storage.drive_id)) end end end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/commands/copy_template_folder_command.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/copy_template_folder_command.rb new file mode 100644 index 00000000000..8dd6b0d69e6 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/copy_template_folder_command.rb @@ -0,0 +1,84 @@ +# 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 Adapters + module Providers + module OneDrive + module Commands + class CopyTemplateFolderCommand < Base + def initialize(storage) + super + @data = Results::CopyTemplateFolder.new(id: nil, polling_url: nil, requires_polling: true) + end + + def call(auth_strategy:, input_data:) + with_tagged_logger do + info "Requesting Copy of folder #{input_data.source} to #{input_data.destination}" + Authentication[auth_strategy].call(storage: @storage) do |httpx| + handle_response( + httpx.post(url_for(input_data.source) + query, json: { name: input_data.destination }) + ) + end + end + end + + private + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 202 } + Success(@data.with(polling_url: response.headers[:location])) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 409 } + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :error)) + end + end + + def url_for(source_location) + UrlBuilder.url(base_uri, "/items", source_location, "/copy") + end + + def query = "?@microsoft.graph.conflictBehavior=fail" + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/commands/create_folder_command.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/create_folder_command.rb new file mode 100644 index 00000000000..b8fb4b633c4 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/create_folder_command.rb @@ -0,0 +1,82 @@ +# 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 Adapters + module Providers + module OneDrive + module Commands + class CreateFolderCommand < Base + def call(auth_strategy:, input_data:) + with_tagged_logger do + info "Creating folder with args: #{input_data.to_h} | #{auth_strategy.value_or({}).to_h}" + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response http.post(url_for(input_data.parent_location), body: payload(input_data.folder_name)) + end + end + end + + private + + def url_for(parent_location) + if parent_location.root? + UrlBuilder.url(base_uri, "/root/children") + else + UrlBuilder.url(base_uri, "/items", parent_location.path, "/children") + end + end + + def handle_response(response) + error = Results::Error.new(payload: response, source: self.class) + + case response + in { status: 200..299 } + info "Folder successfully created." + StorageFileTransformer.new.transform(response.json(symbolize_keys: true)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 409 } + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :error)) + end + end + + def payload(folder_name) + { name: folder_name, folder: {}, "@microsoft.graph.conflictBehavior" => "fail" } + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/failures.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/delete_folder_command.rb similarity index 57% rename from modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/failures.rb rename to modules/storages/app/common/storages/adapters/providers/one_drive/commands/delete_folder_command.rb index b614abadcc0..1c319723ee0 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/failures.rb +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/delete_folder_command.rb @@ -1,4 +1,4 @@ -# frozen_string_literal:true +# frozen_string_literal: true #-- copyright # OpenProject is an open source project management software. @@ -29,31 +29,37 @@ #++ module Storages - module Peripherals - module StorageInteraction - module AuthenticationStrategies - module Failures - Builder = ->(code:, log_message:, data:) do - storage_error = StorageError.new(code:, log_message:, data:) - ServiceResult.failure(result: code, errors: storage_error) - end - - ErrorData = ->(response:, source:) do - payload = - case response - in { content_type: { mime_type: "application/json" } } - response.json - in { content_type: { mime_type: "text/xml" } } - response.xml - else - response.body.to_s + module Adapters + module Providers + module OneDrive + module Commands + class DeleteFolderCommand < Base + def call(auth_strategy:, input_data:) + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response http.delete( + UrlBuilder.url(base_uri, "items", input_data.location) + ) end + end - StorageErrorData.new(source:, payload:) - end + private - TimeoutErrorData = ->(error:, source:) do - StorageErrorData.new(source:, payload: error.to_s) + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + Success() + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 409 } + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :error)) + end + end end end end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/commands/rename_file_command.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/rename_file_command.rb new file mode 100644 index 00000000000..c0f55c870e1 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/rename_file_command.rb @@ -0,0 +1,74 @@ +# 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 Adapters + module Providers + module OneDrive + module Commands + class RenameFileCommand < Base + def call(auth_strategy:, input_data:) + with_tagged_logger do + info "Renaming file: #{input_data.inspect}" + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response( + http.patch(UrlBuilder.url(base_uri, "items", input_data.location), + body: { name: input_data.new_name }) + ) + end + end + end + + private + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + StorageFileTransformer.new.transform(response.json(symbolize_keys: true)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 409 } + Failure(error.with(code: :conflict)) + else + Failure(error.with(code: :error)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/commands/set_permissions_command.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/set_permissions_command.rb new file mode 100644 index 00000000000..28e4cb34897 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/commands/set_permissions_command.rb @@ -0,0 +1,185 @@ +# 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 Adapters + module Providers + module OneDrive + module Commands + class SetPermissionsCommand < Base + include Dry::Monads::Do.for(:call) + + PermissionUpdateData = ::Data.define(:role, :permission_ids, :user_ids, :drive_item_id) do + def create? = permission_ids.empty? && user_ids.any? + + def delete? = permission_ids.any? && user_ids.empty? + + def update? = permission_ids.any? && user_ids.any? + end + + PermissionFilter = lambda do |role, permission| + next unless permission[:roles].member?(role) + + permission[:id] + end.curry + + private_constant :PermissionFilter, :PermissionUpdateData + + # @param auth_strategy [AuthenticationStrategy] The authentication strategy to use. + # @param input_data [Inputs::SetPermissions] The data needed for setting permissions, containing the file id + # and the permissions for an array of users. + def call(auth_strategy:, input_data:) + with_tagged_logger do + Authentication[auth_strategy].call(storage: @storage) do |http| + item = input_data.file_id + yield item_exists?(http, item) + + current_permissions = yield get_current_permissions(http, item) + info "Read and write permissions found: #{current_permissions}" + + role_to_user_map(input_data).each_pair do |role, user_ids| + apply_permission_changes( + PermissionUpdateData.new(role:, + user_ids:, + permission_ids: current_permissions[role], + drive_item_id: item), + http + ) + end + + Success() + end + end + end + + private + + def role_to_user_map(input_data) + input_data.user_permissions + .each_with_object({ read: [], write: [] }) do |user_permission_set, map| + if user_permission_set[:permissions].include?(:write_files) + map[:write] << user_permission_set[:user_id] + elsif user_permission_set[:permissions].include?(:read_files) + map[:read] << user_permission_set[:user_id] + end + end + end + + def item_exists?(http, item_id) + info "Checking if folder #{item_id} exists" + handle_response(http.get(item_path(item_id))) + end + + def get_current_permissions(http, path) + info "Getting current permissions for #{path}" + handle_response(http.get(permissions_path(path))).fmap { |result| extract_permission_ids(result[:value]) } + end + + def apply_permission_changes(update_data, http) + return delete_permissions(update_data, http) if update_data.delete? + return create_permissions(update_data, http) if update_data.create? + + update_permissions(update_data, http) if update_data.update? + end + + def update_permissions(update_data, http) + info "Updating permissions on #{update_data.drive_item_id}" + delete_permissions(update_data, http) + create_permissions(update_data, http) + end + + def create_permissions(update_data, http) + drive_recipients = update_data.user_ids.map { |id| { objectId: id } } + + info "Creating #{update_data.role} permissions on #{update_data.drive_item_id} for #{drive_recipients}" + response = http.post(invite_path(update_data.drive_item_id), + json: { + requireSignIn: true, + sendInvitation: false, + roles: [update_data.role], + recipients: drive_recipients + }) + + handle_response(response).or { |error| log_storage_error(error) } + end + + def delete_permissions(update_data, http) + info "Removing permissions on #{update_data.drive_item_id}" + + update_data.permission_ids.each do |permission_id| + handle_response( + http.delete(permission_path(update_data.drive_item_id, permission_id)) + ).or { |error| log_storage_error(error) } + end + end + + def extract_permission_ids(permission_set) + write_permissions = permission_set.filter_map(&PermissionFilter.call("write")) + read_permissions = permission_set.filter_map(&PermissionFilter.call("read")) + + { read: read_permissions, write: write_permissions } + end + + def handle_response(response) + error = Results::Error.new(payload: response, source: self.class) + + case response + in { status: 200 } + Success(response.json(symbolize_keys: true)) + in { status: 204 } + Success(result: response) + in { status: 400 } + Failure(error.with(code: :bad_request)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 404 } + Failure(error.with(code: :not_found)) + else + Failure(error.with(code: :error)) + end + end + + def permission_path(item_id, permission_id) = "#{permissions_path(item_id)}/#{permission_id}" + + def permissions_path(item_id) = "#{item_path(item_id)}/permissions" + + def invite_path(item_id) = "#{item_path(item_id)}/invite" + + def item_path(item_id) + UrlBuilder.url(base_uri, "/items", item_id) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/managed_folder_identifier.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/managed_folder_identifier.rb new file mode 100644 index 00000000000..556fbeb36b6 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/managed_folder_identifier.rb @@ -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 + module Adapters + module Providers + module OneDrive + class ManagedFolderIdentifier + CHARACTER_BLOCKLIST = /[\\<>+?:"|\/]/ + + def initialize(project_storage) + @project_storage = project_storage + @project = project_storage.project + end + + def name + path + end + + def path + "#{@project.name.gsub(CHARACTER_BLOCKLIST, '_')} (#{@project.id})" + end + + def location + @project_storage.project_folder_id + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/oauth_configuration.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/oauth_configuration.rb new file mode 100644 index 00000000000..dc4e564c75c --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/oauth_configuration.rb @@ -0,0 +1,78 @@ +# 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 Adapters + module Providers + module OneDrive + class OAuthConfiguration < OAuthConfigurationBase + attr_reader :oauth_client + + def initialize(storage) + super() + @storage = storage + raise(ArgumentError, "Storage must have configured OAuth client credentials") if storage.oauth_client.blank? + + @oauth_client = storage.oauth_client.freeze + raise(ArgumentError, "Storage must have a configured tenant id") if storage.tenant_id.blank? + + @oauth_uri = URI("https://login.microsoftonline.com/#{@storage.tenant_id}/oauth2/v2.0").normalize + end + + def to_httpx_oauth_config + AuthenticationStrategies::OAuthConfiguration.new( + client_id: @oauth_client.client_id, + client_secret: @oauth_client.client_secret, + issuer: @oauth_uri, + scope: + ) + end + + def scope + %w[https://graph.microsoft.com/.default offline_access] + end + + def basic_rack_oauth_client + Rack::OAuth2::Client.new( + identifier: @oauth_client.client_id, + redirect_uri: @oauth_client.redirect_uri, + secret: @oauth_client.client_secret, + scheme: @oauth_uri.scheme, + host: @oauth_uri.host, + port: @oauth_uri.port, + authorization_endpoint: "#{@oauth_uri.path}/authorize", + token_endpoint: "#{@oauth_uri.path}/token" + ) + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/one_drive_contract.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/one_drive_contract.rb new file mode 100644 index 00000000000..7a1ef841fc5 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/one_drive_contract.rb @@ -0,0 +1,52 @@ +# 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 Adapters + module Providers + module OneDrive + class OneDriveContract < ::ModelContract + attribute :name + validates :name, presence: true, length: { maximum: 255 } + + attribute :host + validates :host, absence: true + attribute :tenant_id + validates :tenant_id, + format: { with: /\A(?:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|consumers)\z/i } + attribute :drive_id + # GRAPH API considers drive ids of 16 characters or shorter as personal drive ids. Those are not supported, + # and allowing them lead to unexpected behavior. + validates :drive_id, presence: true, allow_nil: true, length: { minimum: 17 } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/one_drive_registry.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/one_drive_registry.rb new file mode 100644 index 00000000000..343f1748d4a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/one_drive_registry.rb @@ -0,0 +1,98 @@ +# 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 Adapters + module Providers + module OneDrive + OneDriveRegistry = Dry::Container::Namespace.new("one_drive") do + namespace("authentication") do + register(:userless, ->(use_cache = true) { Input::Strategy.build(key: :oauth_client_credentials, use_cache:) }) + register(:user_bound, ->(user, storage = nil) { Input::Strategy.build(key: :oauth_user_token, user:, storage:) }) + end + + namespace("commands") do + register(:copy_template_folder, Commands::CopyTemplateFolderCommand) + register(:create_folder, Commands::CreateFolderCommand) + register(:delete_folder, Commands::DeleteFolderCommand) + register(:rename_file, Commands::RenameFileCommand) + register(:set_permissions, Commands::SetPermissionsCommand) + end + + namespace("components") do + namespace("forms") do + register(:access_management, Admin::Forms::AccessManagementFormComponent) + register(:general_information, Admin::Forms::GeneralInfoFormComponent) + register(:oauth_client, Admin::Forms::OAuthClientFormComponent) + register(:redirect_uri, Admin::Forms::RedirectUriFormComponent) + end + + register(:setup_wizard, StorageWizard) + + register(:access_management, Admin::AccessManagementComponent) + register(:general_information, Admin::GeneralInfoComponent) + register(:oauth_client, Admin::OAuthClientInfoComponent) + register(:redirect_uri, Admin::RedirectUriComponent) + end + + namespace("contracts") do + register(:storage, OneDriveContract) + register(:general_information, OneDriveContract) + end + + namespace("models") do + register(:managed_folder_identifier, ManagedFolderIdentifier) + end + + namespace("queries") do + register(:download_link, Queries::DownloadLinkQuery) + register(:file_info, Queries::FileInfoQuery) + register(:file_path_to_id_map, Queries::FilePathToIdMapQuery) + register(:files, Queries::FilesQuery) + register(:files_info, Queries::FilesInfoQuery) + register(:open_file_link, Queries::OpenFileLinkQuery) + register(:open_storage, Queries::OpenStorageQuery) + register(:upload_link, Queries::UploadLinkQuery) + register(:user, Queries::UserQuery) + end + + namespace("services") do + register(:upkeep_managed_folders, NextcloudManagedFolderCreateService) + register(:upkeep_managed_folder_permissions, NextcloudManagedFolderPermissionsService) + end + + namespace("validators") do + register(:connection, Validators::ConnectionValidator) + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/download_link_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/download_link_query.rb new file mode 100644 index 00000000000..8ae1d2cb3f9 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/download_link_query.rb @@ -0,0 +1,70 @@ +# 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 Adapters + module Providers + module OneDrive + module Queries + class DownloadLinkQuery < Base + def call(auth_strategy:, input_data:) + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_errors http.get(url_for(input_data.file_link.origin_id)) + end + end + + private + + def handle_errors(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 300..399 } + Success(URI(response.headers["Location"])) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + def url_for(file_id) + UrlBuilder.url(base_uri, "items", file_id, "content") + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/file_info_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/file_info_query.rb new file mode 100644 index 00000000000..780ebe70c6a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/file_info_query.rb @@ -0,0 +1,76 @@ +# 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 Adapters + module Providers + module OneDrive + module Queries + class FileInfoQuery < Base + FIELDS = %w[id name fileSystemInfo file folder size createdBy lastModifiedBy parentReference].freeze + + def call(auth_strategy:, input_data:) + base_query = Authentication[auth_strategy].call(storage: @storage) do |http| + drive_item_query.call(http:, drive_item_id: input_data.file_id, fields: FIELDS) + end + + result = base_query.fmap { |json| storage_file_info(json) } + + result.or do |error| + return Failure(error) unless error.code == :not_found && auth_strategy.value!.user.present? + + admin_query(input_data.file_id) + end + end + + private + + def admin_query(file_id) + Authentication[userless_strategy].call(storage: @storage) do |http| + drive_item_query.call(http:, drive_item_id: file_id, fields: FIELDS) + .bind { |json| storage_file_info(json, status: "forbidden", status_code: 403) } + end + end + + def userless_strategy = Registry.resolve("one_drive.authentication.userless").call + + def drive_item_query + @drive_item_query ||= Internal::DriveItemQuery.new(@storage) + end + + def storage_file_info(json, status: "ok", status_code: 200) + StorageFileTransformer.new.transform_file_info({ status:, status_code: }.merge(json)) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/file_path_to_id_map_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/file_path_to_id_map_query.rb new file mode 100644 index 00000000000..15655d78e9f --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/file_path_to_id_map_query.rb @@ -0,0 +1,116 @@ +# 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 Adapters + module Providers + module OneDrive + module Queries + class FilePathToIdMapQuery < Base + CHILDREN_FIELDS = %w[id name file folder parentReference].freeze + FOLDER_FIELDS = %w[id name parentReference].freeze + + def initialize(*) + super + @drive_item_query = Internal::DriveItemQuery.new(@storage) + @children_query = Internal::ChildrenQuery.new(@storage) + end + + def call(auth_strategy:, input_data:) + Authentication[auth_strategy].call(storage: @storage) do |http| + fetch_folder(http, input_data.folder).bind do |file_ids_dictionary| + queue = [input_data.folder] + level = 0 + + while queue.any? && level < input_data.depth + visit(http, queue.shift).bind do |info| + entry, to_queue = info.values_at(:entry, :to_queue) + file_ids_dictionary.merge!(entry) + queue += to_queue + level += 1 + end + end + + Success(file_ids_dictionary) + end + end + end + + private + + def visit(http, folder) + @children_query.call(http:, folder:, fields: CHILDREN_FIELDS).bind do |call| + entries = {} + + to_queue = call.filter_map do |json| + entry, folder = parse_drive_item_info(json).values_at(:entry, :folder) + + entries.merge!(entry) + next if folder.blank? + + folder + end + + Success({ entry: entries, to_queue: }) + end + end + + def parse_drive_item_info(json) + drive_item_id = json[:id] + location = extract_location(json[:parentReference], json[:name]) + + entry = { location => StorageFileId.new(id: drive_item_id) } + folder = json[:folder].present? ? Peripherals::ParentFolder.new(drive_item_id) : nil + + { entry:, folder: } + end + + def extract_location(parent_reference, file_name = "") + location = parent_reference[:path].gsub(/.*root:/, "") + + appendix = file_name.blank? ? "" : "/#{file_name}" + location.empty? ? "/#{file_name}" : "#{location}#{appendix}" + end + + def fetch_folder(http, folder) + @drive_item_query.call(http:, drive_item_id: folder.path, fields: FOLDER_FIELDS).fmap do |json| + if folder.root? + { "/" => StorageFileId.new(id: json[:id]) } + else + parse_drive_item_info(json)[:entry] + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/sso_user_token.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/files_info_query.rb similarity index 53% rename from modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/sso_user_token.rb rename to modules/storages/app/common/storages/adapters/providers/one_drive/queries/files_info_query.rb index 3b9147e4f23..cdb4df70f0b 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/sso_user_token.rb +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/files_info_query.rb @@ -1,4 +1,4 @@ -# frozen_string_literal:true +# frozen_string_literal: true #-- copyright # OpenProject is an open source project management software. @@ -29,32 +29,38 @@ #++ module Storages - module Peripherals - module StorageInteraction - module AuthenticationStrategies - class SsoUserToken - def self.strategy - Strategy.new(:sso_user_token) - end + module Adapters + module Providers + module OneDrive + module Queries + class FilesInfoQuery < Base + def call(auth_strategy:, input_data:) + with_tagged_logger do + info "Retrieving file information for #{input_data.file_ids.join(', ')}" - def initialize(user) - @user = user - end + infos = input_data.file_ids.map do |file_id| + Input::FileInfo.build(file_id:).bind do |file_data| + FileInfoQuery.call(storage: @storage, auth_strategy:, input_data: file_data).value_or do |failure| + return failure if failure.source.module_parent == Authentication - def call(storage:, http_options: {}, &) - OpenIDConnect::UserTokens::FetchService - .new(user: @user, exchange_scope: storage.token_exchange_scope) - .access_token_for(audience: storage.audience) - .either( - ->(token) do - opts = http_options.deep_merge({ headers: { "Authorization" => "Bearer #{token}" } }) - yield OpenProject.httpx.with(opts) - end, - ->(error) do - log_message = "Failed to fetch access token for user #{@user}. Error: #{error.inspect}" - Failures::Builder.call(code: :unauthorized, log_message:, data: error) + wrap_storage_file_error(file_data.file_id, failure) + end + end end + + Success(infos) + end + end + + private + + def wrap_storage_file_error(file_id, query_result) + Results::StorageFileInfo.new( + id: file_id, + status: query_result.code, + status_code: Rack::Utils::SYMBOL_TO_STATUS_CODE[query_result.code] || 500 ) + end end end end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/files_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/files_query.rb new file mode 100644 index 00000000000..2489c7095bf --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/files_query.rb @@ -0,0 +1,144 @@ +# 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 Adapters + module Providers + module OneDrive + module Queries + class FilesQuery < Base + FIELDS = "?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference" + + def initialize(*) + super + @transformer = StorageFileTransformer.new + end + + def call(auth_strategy:, input_data:) + with_tagged_logger do + info "Getting data on all files under folder '#{input_data.folder}'" + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response(http.get(children_url_for(input_data.folder) + FIELDS)).bind do |response| + files = response.fetch(:value, []) + return empty_response(http, input_data.folder) if files.empty? + + storage_files(files) + end + end + end + end + + private + + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + Success(response.json(symbolize_keys: true)) + in { status: 400 } + Failure(error.with(code: :request_error)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + def storage_files(json_files) + files = json_files.map { |json| @transformer.transform(json).value_or(nil) } + + parent_reference = json_files.first[:parentReference] + Results::StorageFileCollection + .build(files: files.compact, parent: parent(parent_reference), ancestors: forge_ancestors(parent_reference)) + end + + def empty_response(http, folder) + handle_response(http.get(location_url_for(folder) + FIELDS)).bind do |response| + empty_storage_files(folder.path, response[:id]) + end + end + + def empty_storage_files(path, parent_id) + Results::StorageFileCollection.build( + files: [], + parent: @transformer.bare_transform(id: parent_id, location: path), + ancestors: forge_ancestors(path:) + ) + end + + def parent(parent_reference) + _, _, name = parent_reference[:path].gsub(/.*root:/, "").rpartition "/" + + if name.empty? + root(parent_reference[:id]) + else + @transformer.parent_transform(id: parent_reference[:id], name:, location: parent_reference) + end + end + + def forge_ancestors(parent_reference) + path_elements = parent_reference[:path].gsub(/.+root:/, "").split("/") + + path_elements[0..-2].map do |component| + next root(Digest::SHA256.hexdigest("i_am_root")) if component.blank? + + Results::StorageFile.new( + id: Digest::SHA256.hexdigest(component), + name: component, + location: UrlBuilder.path(component) + ) + end + end + + def root(id) = Results::StorageFile.new(name: "Root", location: "/", id:, permissions: %i[readable writeable]) + + def children_url_for(folder) + return UrlBuilder.url(base_uri, "/root/children") if folder.root? + + "#{UrlBuilder.url(base_uri, '/root')}:#{UrlBuilder.path(folder.path)}:/children" + end + + def location_url_for(folder) + base_url = UrlBuilder.url(base_uri, "/root") + return base_url if folder.root? + + "#{base_url}:#{UrlBuilder.path(folder.path)}" + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/internal/children_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/internal/children_query.rb new file mode 100644 index 00000000000..835f931e7bb --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/internal/children_query.rb @@ -0,0 +1,75 @@ +# 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 Adapters + module Providers + module OneDrive + module Queries + module Internal + class ChildrenQuery < Base + def call(http:, folder:, fields: []) + query = fields.empty? ? "" : "?$select=#{fields.join(',')}" + + url = UrlBuilder.url(base_uri, uri_path_for(folder)) + handle_responses(http.get(url + query)) + end + + private + + def handle_responses(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + Success(response.json(symbolize_keys: true).fetch(:value)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + def uri_path_for(folder) + return "/root/children" if folder.root? + + "/items/#{folder.path}/children" + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/internal/drive_item_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/internal/drive_item_query.rb new file mode 100644 index 00000000000..280261683b0 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/internal/drive_item_query.rb @@ -0,0 +1,85 @@ +# 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 Adapters + module Providers + module OneDrive + module Queries + module Internal + class DriveItemQuery < Base + def call(http:, drive_item_id:, fields: []) + select_url_query = if fields.empty? + "" + else + "?$select=#{fields.join(',')}" + end + + make_file_request(drive_item_id, http, select_url_query) + end + + private + + def make_file_request(drive_item_id, http, select_url_query) + url = UrlBuilder.url(base_uri, uri_path_for(drive_item_id)) + handle_response http.get("#{url}#{select_url_query}") + end + + def handle_response(response) + error = Results::Error.new(payload: response, source: self.class) + + case response + in { status: 200..299 } + Success(response.json(symbolize_keys: true)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + def uri_path_for(file_id) + if file_id == "/" + "/root" + else + "/items/#{file_id}" + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/open_file_link_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/open_file_link_query.rb new file mode 100644 index 00000000000..8fd6e703267 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/open_file_link_query.rb @@ -0,0 +1,65 @@ +# 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 Adapters + module Providers + module OneDrive + module Queries + class OpenFileLinkQuery < Base + def call(auth_strategy:, input_data:) + Authentication[auth_strategy].call(storage: @storage) do |http| + if input_data.open_location + request_parent_id(http, input_data.file_id).bind { request_web_url(http, it) } + else + request_web_url(http, input_data.file_id) + end + end + end + + private + + def drive_item_query + @drive_item_query ||= Internal::DriveItemQuery.new(@storage) + end + + def request_web_url(http, file_id) + drive_item_query.call(http:, drive_item_id: file_id, fields: %w[webUrl]).fmap { it[:webUrl] } + end + + def request_parent_id(http, file_id) + drive_item_query.call(http:, drive_item_id: file_id, fields: %w[parentReference]).fmap { it.dig(:parentReference, :id) } + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/authentication_validator.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/open_storage_query.rb similarity index 55% rename from modules/storages/app/common/storages/peripherals/connection_validators/one_drive/authentication_validator.rb rename to modules/storages/app/common/storages/adapters/providers/one_drive/queries/open_storage_query.rb index ebeb60c4a01..23851206af8 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/authentication_validator.rb +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/open_storage_query.rb @@ -29,43 +29,42 @@ #++ module Storages - module Peripherals - module ConnectionValidators + module Adapters + module Providers module OneDrive - class AuthenticationValidator < BaseValidatorGroup - def self.key = :authentication - - def initialize(storage) - super - @user = User.current - end - - private - - def validate - register_checks(:existing_token, :user_bound_request) - - oauth_token - user_bound_request - end - - def oauth_token - if OAuthClientToken.for_user_and_client(@user, @storage.oauth_client).exists? - pass_check(:existing_token) - else - warn_check(:existing_token, :od_oauth_token_missing, halt_validation: true) - end - end - - def user_bound_request - Registry["one_drive.queries.user"].call(storage: @storage, auth_strategy:).on_failure do - fail_check(:user_bound_request, :"od_oauth_request_#{it.result}") + module Queries + class OpenStorageQuery < Base + def call(auth_strategy:, **) + Authentication[auth_strategy].call(storage: @storage) do |http| + request_drive(http).fmap { it[:webUrl] } + end end - pass_check(:user_bound_request) - end + private - def auth_strategy = Registry["one_drive.authentication.user_bound"].call(storage: @storage, user: @user) + def request_drive(http) + handle_responses http.get(request_url) + end + + def handle_responses(response) + error = Results::Error.new(source: self.class, payload: @storage) + + case response + in { status: 200..299 } + Success(response.json(symbolize_keys: true)) + in { status: 404 } + Failure(error.with(code: :not_found)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + else + Failure(error.with(code: :error)) + end + end + + def request_url = "#{base_uri}?$select=webUrl" + end end end end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/upload_link_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/upload_link_query.rb new file mode 100644 index 00000000000..a81e63cff0e --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/upload_link_query.rb @@ -0,0 +1,91 @@ +# 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 Adapters + module Providers + module OneDrive + module Queries + class UploadLinkQuery < Base + def call(auth_strategy:, input_data:) + with_tagged_logger do + Authentication[auth_strategy].call(storage: @storage) do |http| + info "Requesting an upload link on folder #{input_data.folder_id}" + handle_response( + http.post(url(input_data.folder_id, input_data.file_name), json: payload(input_data.file_name)) + ) + end + end + end + + private + + def payload(filename) + { item: { "@microsoft.graph.conflictBehavior" => "rename", name: filename } } + end + + # rubocop:disable Metrics/AbcSize + def handle_response(response) + error = Results::Error.new(source: self.class, payload: response) + + case response + in { status: 200..299 } + upload_url = response.json(symbolize_keys: true)[:uploadUrl] + info "Upload link generated successfully." + Results::UploadLink.build(destination: upload_url, method: :put) + in { status: 404 | 400 } # not existent parent folder in request url is responded with 400 + info "The parent folder was not found." + Failure(error.with(code: :not_found)) + in { status: 401 } + info "User authorization failed." + Failure(error.with(code: :unauthorized)) + in { status: 403 } + info "User authorization failed." + Failure(error.with(code: :forbidden)) + else + info "Unknown error happened." + Failure(error.with(code: :error)) + end + end + + # rubocop:enable Metrics/AbcSize + + def url(folder, filename) + base = UrlBuilder.url(base_uri, "/items/", folder) + file_path = UrlBuilder.path(filename) + + "#{base}:#{file_path}:/createUploadSession" + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/queries/user_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/user_query.rb new file mode 100644 index 00000000000..58fc5c444ad --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/queries/user_query.rb @@ -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 + module Adapters + module Providers + module OneDrive + module Queries + class UserQuery < Base + def self.call(storage:, auth_strategy:) + new(storage).call(auth_strategy:) + end + + def call(auth_strategy:) + Authentication[auth_strategy].call(storage: @storage) do |http| + handle_response http.get(UrlBuilder.url(@storage.uri, "/v1.0/me")) + end + end + + private + + def handle_response(response) + error = Results::Error.new(payload: response, source: self.class) + + case response + in { status: 200..299 } + # FIXME: Make this into a Result::RemoteUserId - 2025-03-18 @mereghost + Success(id: response.json["id"]) + in { status: 401 } + Failure(error.with(code: :unauthorized)) + in { status: 403 } + Failure(error.with(code: :forbidden)) + else + Failure(error.with(code: :error)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/storage_file_transformer.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/storage_file_transformer.rb new file mode 100644 index 00000000000..2d8fdfe7d2a --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/storage_file_transformer.rb @@ -0,0 +1,98 @@ +# 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 Adapters + module Providers + module OneDrive + class StorageFileTransformer + def transform(json) + Results::StorageFile.build( + id: json[:id], + name: json[:name], + size: json[:size], + mime_type: mime_type(json), + created_at: Time.zone.parse(json.dig(:fileSystemInfo, :createdDateTime)), + last_modified_at: Time.zone.parse(json.dig(:fileSystemInfo, :lastModifiedDateTime)), + created_by_name: json.dig(:createdBy, :user, :displayName), + last_modified_by_name: json.dig(:lastModifiedBy, :user, :displayName), + location: UrlBuilder.path(extract_location(json[:parentReference], json[:name])), + permissions: %i[readable writeable] + ) + end + + def bare_transform(id:, location:) + Results::StorageFile.new(id:, name: location.split("/").last, location: UrlBuilder.path(location), + permissions: %i[readable writeable]) + end + + def parent_transform(id:, location:, name:) + Results::StorageFile.new(id:, name:, location: UrlBuilder.path(extract_location(location)), + permissions: %i[readable writeable]) + end + + # rubocop:disable Metrics/AbcSize + def transform_file_info(json) + # Need to handle the errors + Results::StorageFileInfo.build( + status: json[:status], + status_code: json[:status_code], + id: json[:id], + name: json[:name], + mime_type: mime_type(json), + size: json[:size], + owner_name: json.dig(:createdBy, :user, :displayName), + owner_id: json.dig(:createdBy, :user, :id), + location: UrlBuilder.path(extract_location(json[:parentReference], json[:name])), + last_modified_at: json.dig(:fileSystemInfo, :lastModifiedDateTime), + created_at: json.dig(:fileSystemInfo, :createdDateTime), + last_modified_by_name: json.dig(:lastModifiedBy, :user, :displayName), + last_modified_by_id: json.dig(:lastModifiedBy, :user, :id) + ).value_or(nil) + end + # rubocop:enable Metrics/AbcSize + + private + + def mime_type(json) + json.dig(:file, :mimeType) || (json.key?(:folder) ? "application/x-op-directory" : nil) + end + + def extract_location(parent_reference, file_name = "") + location = parent_reference[:path].gsub(/.*root:/, "") + + appendix = file_name.blank? ? "" : "/#{file_name}" + location.empty? ? "/#{file_name}" : "#{location}#{appendix}" + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/storage_wizard.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/storage_wizard.rb new file mode 100644 index 00000000000..711cbed99d7 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/storage_wizard.rb @@ -0,0 +1,79 @@ +# 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 Adapters + module Providers + module OneDrive + class StorageWizard < Wizard + step :general_information, completed_if: ->(storage) { storage.name.present? } + + step :access_management, + section: :access_management_section, + completed_if: ->(storage) { !storage.automatic_management_unspecified? }, + preparation: :prepare_storage_for_access_management_form + + step :oauth_client, + section: :oauth_configuration, + completed_if: ->(storage) { storage.oauth_client.present? }, + preparation: ->(storage) { storage.build_oauth_client } + + step :redirect_uri, + section: :oauth_configuration, + completed_if: ->(storage) { + # Working around the fact that there is nothing changed on the storage after showing + # the redirect url. The redirect URL step only exists to show the oauth client's redirect + # URL to the user right after the client was created. + storage.oauth_client&.persisted? && storage.oauth_client.created_at < 10.seconds.ago + } + + private + + def prepare_storage_for_access_management_form(storage) + ::Storages::Storages::SetProviderFieldsAttributesService + .new(user:, model: storage, contract_class: EmptyContract) + .call + end + + def prepare_oauth_application(storage) + persist_service_result = ::Storages::OAuthApplications::CreateService.new(storage:, user:).call + storage.oauth_application = persist_service_result.result if persist_service_result.success? + end + + def prepare_storage_for_automatic_management_form(storage) + ::Storages::Storages::SetProviderFieldsAttributesService + .new(user:, model: storage, contract_class: EmptyContract) + .call + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/validators/ampf_configuration_validator.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/validators/ampf_configuration_validator.rb new file mode 100644 index 00000000000..9f6cbef9be8 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/validators/ampf_configuration_validator.rb @@ -0,0 +1,119 @@ +# 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 Adapters + module Providers + module OneDrive + module Validators + class AmpfConfigurationValidator < ConnectionValidators::BaseValidatorGroup + TEST_FOLDER_NAME = "OpenProjectConnectionValidationFolder" + + def self.key = :ampf_configuration + + private + + def validate + register_checks :client_folder_creation, :client_folder_removal, :drive_contents + + client_permissions + unexpected_content + end + + def unexpected_content + files = files_query.value_or { fail_check(:drive_contents, :unknown_error) } + unexpected_files = files.reject { managed_project_folder_ids.include?(it.id) } + + if unexpected_files.empty? + pass_check(:drive_contents) + else + log_extraneous_files(unexpected_files) + warn_check(:drive_contents, :od_unexpected_content) + end + end + + # Testing setting permissions and checking permission inheritance would be great, + # but there are some challenges to it. We need to figure out a good way to go about this + # 2025-04-08 @mereghost + def client_permissions + folder = create_folder.value! + delete_folder(folder) + end + + def delete_folder(folder) + Input::DeleteFolder.build(location: folder.id).bind do |input_data| + Registry["one_drive.commands.delete_folder"].call(storage: @storage, auth_strategy:, input_data:) + .either(->(_) { pass_check(:client_folder_removal) }, + ->(_) { fail_check(:client_folder_removal, :od_client_cant_delete_folder) }) + end + end + + def create_folder + Input::CreateFolder.build(folder_name: TEST_FOLDER_NAME, parent_location: "/").bind do |input_data| + folder_result = Registry["one_drive.commands.create_folder"].call(storage: @storage, auth_strategy:, input_data:) + + folder_result.either( + ->(_) { pass_check(:client_folder_creation) }, + ->(error) do + code = error.code == :conflict ? :od_existing_test_folder : :od_client_write_permission_missing + fail_check(:client_folder_creation, code, context: { folder_name: TEST_FOLDER_NAME }) + end + ) + + folder_result + end + end + + def log_extraneous_files(unexpected_files) + file_representation = unexpected_files.map do |file| + "Name: #{file.name}, ID: #{file.id}, Location: #{file.location}" + end + + warn "Unexpected files/folder found in group folder:\n\t#{file_representation.join("\n\t")}" + end + + def managed_project_folder_ids + @managed_project_folder_ids ||= ProjectStorage.automatic.where(storage: @storage) + .pluck(:project_folder_id).to_set + end + + def files_query + Input::Files + .build(folder: "/") + .bind { Registry["one_drive.queries.files"].call(storage: @storage, auth_strategy:, input_data: it) } + end + + def auth_strategy = Registry["one_drive.authentication.userless"].call + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/validators/authentication_validator.rb similarity index 53% rename from modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query.rb rename to modules/storages/app/common/storages/adapters/providers/one_drive/validators/authentication_validator.rb index 515a0606640..7547096462d 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query.rb +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/validators/authentication_validator.rb @@ -29,39 +29,43 @@ #++ module Storages - module Peripherals - module StorageInteraction + module Adapters + module Providers module OneDrive - class OpenFileLinkQuery - def self.call(storage:, auth_strategy:, file_id:, open_location: false) - new(storage).call(auth_strategy:, file_id:, open_location:) - end + module Validators + class AuthenticationValidator < ConnectionValidators::BaseValidatorGroup + def self.key = :authentication - def initialize(storage) - @storage = storage - @delegate = Internal::DriveItemQuery.new(storage) - end + def initialize(storage) + super + @user = User.current + end - def call(auth_strategy:, file_id:, open_location: false) - Authentication[auth_strategy].call(storage: @storage) do |http| - if open_location - request_parent_id(http, file_id).on_success { |parent_id| return request_web_url(http, parent_id.result) } + private + + def validate + register_checks(:existing_token, :user_bound_request) + + oauth_token + user_bound_request + end + + def oauth_token + if OAuthClientToken.for_user_and_client(@user, @storage.oauth_client).exists? + pass_check(:existing_token) else - request_web_url(http, file_id) + warn_check(:existing_token, :od_oauth_token_missing, halt_validation: true) end end - end - private + def user_bound_request + Registry["one_drive.queries.user"].call(storage: @storage, auth_strategy:).either( + ->(_) { pass_check(:user_bound_request) }, + -> { fail_check(:user_bound_request, :"od_oauth_request_#{it.code}") } + ) + end - # rubocop:disable Rails/Pluck - def request_web_url(http, file_id) - @delegate.call(http:, drive_item_id: file_id, fields: %w[webUrl]).map { |json| json[:webUrl] } - end - # rubocop:enable Rails/Pluck - - def request_parent_id(http, file_id) - @delegate.call(http:, drive_item_id: file_id, fields: %w[parentReference]).map { |json| json.dig(:parentReference, :id) } + def auth_strategy = Registry["one_drive.authentication.user_bound"].call(@user, @storage) end end end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/validators/connection_validator.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/validators/connection_validator.rb new file mode 100644 index 00000000000..271c7a83e47 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/validators/connection_validator.rb @@ -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 + module Adapters + module Providers + module OneDrive + module Validators + class ConnectionValidator < ConnectionValidators::BaseConnectionValidator + register_group StorageConfigurationValidator + register_group AuthenticationValidator, + precondition: ->(_, result) { result.group(:base_configuration).non_failure? } + register_group AmpfConfigurationValidator, + precondition: ->(storage, result) { + result.group(:base_configuration).non_failure? && storage.automatic_management_enabled? + } + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/providers/one_drive/validators/storage_configuration_validator.rb b/modules/storages/app/common/storages/adapters/providers/one_drive/validators/storage_configuration_validator.rb new file mode 100644 index 00000000000..00a3022b280 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/providers/one_drive/validators/storage_configuration_validator.rb @@ -0,0 +1,146 @@ +# 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 Adapters + module Providers + module OneDrive + module Validators + class StorageConfigurationValidator < ConnectionValidators::BaseValidatorGroup + def self.key = :base_configuration + + private + + def validate + register_checks :storage_configured, + :diagnostic_request, + :tenant_id, + :client_secret, + :client_id, + :drive_id_format, + :drive_id_exists + + storage_configuration_status + diagnostic_request + check_tenant_id + check_client_secret + check_client_id + malformed_drive_id + drive_not_found + end + + def malformed_drive_id + return pass_check(:drive_id_format) if query_result.success? + + if error_payload.dig(:error, :code) == "invalidRequest" + fail_check(:drive_id_format, :od_drive_id_invalid) + else + pass_check(:drive_id_format) + end + end + + def drive_not_found + if query_result.failure? && @query_result.failure.code == :not_found + fail_check(:drive_id_exists, :od_drive_id_not_found) + else + pass_check(:drive_id_exists) + end + end + + def check_tenant_id + return pass_check(:tenant_id) if query_result.success? + + tenant_id_regex = /tenant (?:identifier )?'#{@storage.tenant_id}' (?:not found|is neither)/i + + if error_payload[:error] == "invalid_request" && error_payload[:error_description].match?(tenant_id_regex) + fail_check(:tenant_id, :od_tenant_id_invalid) + else + pass_check(:tenant_id) + end + end + + def check_client_id + return pass_check(:client_id) if query_result.success? + + if error_payload[:error] == "unauthorized_client" + fail_check(:client_id, :client_id_invalid) + else + pass_check(:client_id) + end + end + + def check_client_secret + return pass_check(:client_secret) if query_result.success? + + if error_payload[:error] == "invalid_client" + fail_check(:client_secret, :client_secret_invalid) + else + pass_check(:client_secret) + end + end + + def diagnostic_request + if query_result.failure? && query_result.failure.code == :error + error "Connection validation failed with unknown error:\n" \ + "\tstorage: ##{@storage.id} #{@storage.name}\n" \ + "\tstatus: #{query_result.failure}\n" \ + "\tresponse: #{query_result.failure.payload}" + + fail_check(:diagnostic_request, :unknown_error) + else + pass_check :diagnostic_request + end + end + + def storage_configuration_status + if @storage.configured? + pass_check(:storage_configured) + else + fail_check(:storage_configured, :not_configured) + end + end + + def query_result + @query_result ||= Input::Files.build(folder: "/").bind do |input_data| + Registry["#{@storage}.queries.files"].call(storage: @storage, auth_strategy:, input_data:) + end + end + + def auth_strategy = Registry["one_drive.authentication.userless"].call + + def error_payload + @error_payload ||= query_result.either(->(_) { {} }, -> { MultiJson.load(it.payload, symbolize_keys: true) }) + end + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/registry.rb b/modules/storages/app/common/storages/adapters/registry.rb similarity index 72% rename from modules/storages/app/common/storages/peripherals/registry.rb rename to modules/storages/app/common/storages/adapters/registry.rb index f0d3c125a42..646de98100c 100644 --- a/modules/storages/app/common/storages/peripherals/registry.rb +++ b/modules/storages/app/common/storages/adapters/registry.rb @@ -28,29 +28,40 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require "dry/auto_inject" +require "dry/container" module Storages - module Peripherals + module Adapters class Registry extend Dry::Container::Mixin + # Extracts the known_providers from the registered keys + # @return [Array] + def self.known_providers + keys.map { it.split(".").first }.uniq + end + class Resolver < Dry::Container::Resolver include TaggedLogging + def call(container, key) - with_tagged_logger("Registry") do + with_tagged_logger("Adapters::Registry") do info "Resolving #{key}" super end rescue Dry::Container::KeyError - raise Errors.registry_error_for(key) + error = Errors.registry_error_for(key) + + with_tagged_logger("Adapters::Registry") { error error.message } + raise error end end config.resolver = Resolver.new - end - Registry.import NextcloudRegistry - Registry.import OneDriveRegistry + # Need to make this dynamic to ease new providers to be registered + import Providers::OneDrive::OneDriveRegistry + import Providers::Nextcloud::NextcloudRegistry + end end end diff --git a/modules/storages/app/common/storages/adapters/results/copy_template_folder.rb b/modules/storages/app/common/storages/adapters/results/copy_template_folder.rb new file mode 100644 index 00000000000..2237d32525d --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/copy_template_folder.rb @@ -0,0 +1,39 @@ +# 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 Adapters + module Results + CopyTemplateFolder = Data.define(:id, :polling_url, :requires_polling) do + def requires_polling? = !!requires_polling + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/results/error.rb b/modules/storages/app/common/storages/adapters/results/error.rb new file mode 100644 index 00000000000..1e3d671d6e1 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/error.rb @@ -0,0 +1,41 @@ +# 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 Adapters + module Results + Error = ::Data.define(:code, :payload, :source) do + def initialize(source:, code: nil, payload: nil) + super + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/results/storage_file.rb b/modules/storages/app/common/storages/adapters/results/storage_file.rb new file mode 100644 index 00000000000..36083dee2fc --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/storage_file.rb @@ -0,0 +1,71 @@ +# 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 Adapters + module Results + StorageFile = Data.define( + :id, + :name, + :size, + :mime_type, + :created_at, + :last_modified_at, + :created_by_name, + :last_modified_by_name, + :location, + :permissions + ) do + def initialize( + id:, + name:, + size: nil, + mime_type: nil, + created_at: nil, + last_modified_at: nil, + created_by_name: nil, + last_modified_by_name: nil, + location: nil, + permissions: nil + ) + super + end + + def self.build(contract: StorageFileContract.new, **) + contract.call(**).to_monad.fmap { |input| new(**input.to_h) } + end + + def folder? + mime_type.present? && mime_type == "application/x-op-directory" + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/managed_folder_identifier/nextcloud.rb b/modules/storages/app/common/storages/adapters/results/storage_file_collection.rb similarity index 76% rename from modules/storages/app/common/storages/peripherals/managed_folder_identifier/nextcloud.rb rename to modules/storages/app/common/storages/adapters/results/storage_file_collection.rb index 553b2f5c8eb..a5ccb4b9d48 100644 --- a/modules/storages/app/common/storages/peripherals/managed_folder_identifier/nextcloud.rb +++ b/modules/storages/app/common/storages/adapters/results/storage_file_collection.rb @@ -29,24 +29,17 @@ #++ module Storages - module Peripherals - module ManagedFolderIdentifier - class Nextcloud - def initialize(project_storage) - @storage = project_storage.storage - @project = project_storage.project + module Adapters + module Results + StorageFileCollection = Data.define(:files, :parent, :ancestors) do + delegate :reject, to: :files + + def self.build(files:, parent:, ancestors:, contract: StorageFileCollectionContract.new) + contract.call(files:, parent:, ancestors:).to_monad.fmap { |it| new(**it.to_h) } end - def name - "#{@project.name.tr('/', '|')} (#{@project.id})" - end - - def path - "/#{@storage.group_folder}/#{name}/" - end - - def location - path + def all_folders + files.filter(&:folder?) end end end diff --git a/modules/storages/app/common/storages/adapters/results/storage_file_collection_contract.rb b/modules/storages/app/common/storages/adapters/results/storage_file_collection_contract.rb new file mode 100644 index 00000000000..65158251015 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/storage_file_collection_contract.rb @@ -0,0 +1,43 @@ +# 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 Adapters + module Results + class StorageFileCollectionContract < Dry::Validation::Contract + params do + required(:files).array(AdapterTypes::StorageFileInstance) + required(:parent).filled(AdapterTypes::StorageFileInstance) + required(:ancestors).array(AdapterTypes::StorageFileInstance) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/results/storage_file_contract.rb b/modules/storages/app/common/storages/adapters/results/storage_file_contract.rb new file mode 100644 index 00000000000..299c8700cc7 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/storage_file_contract.rb @@ -0,0 +1,55 @@ +# 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 Adapters + module Results + class StorageFileContract < Dry::Validation::Contract + params do + before(:value_coercer) do |input| + input.to_h.compact + end + + required(:id).filled(:string) + required(:name).filled(:string) + optional(:size).filled(:integer, gteq?: 0) + optional(:mime_type).filled(:string) + optional(:created_at).filled(:time) + optional(:last_modified_at).filled(:time) + optional(:created_by_name).filled(:string) + optional(:last_modified_by_name).filled(:string) + optional(:location).filled(:string, format?: /^\//) + # Permissions are either array or string. We should standardise this + optional(:permissions).value { array? | str? } + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/results/storage_file_info.rb b/modules/storages/app/common/storages/adapters/results/storage_file_info.rb new file mode 100644 index 00000000000..256cb076130 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/storage_file_info.rb @@ -0,0 +1,92 @@ +# 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 Adapters + module Results + # TODO: Unify StorageFile and StorageFileInfo as one is a subset of the other + StorageFileInfo = Data.define( + :status, + :status_code, + :id, + :name, + :last_modified_at, + :created_at, + :mime_type, + :size, + :owner_name, + :owner_id, + :last_modified_by_name, + :last_modified_by_id, + :permissions, + :location + ) do + def initialize( + status:, + status_code:, + id:, + name: nil, + size: nil, + mime_type: nil, + created_at: nil, + last_modified_at: nil, + last_modified_by_name: nil, + location: nil, + permissions: nil, + owner_name: nil, + owner_id: nil, + last_modified_by_id: nil + ) + super + end + + def self.build(contract: StorageFileInfoContract.new, **) + contract.call(**).to_monad.fmap { new(**it.to_h) } + end + + def to_storage_file = StorageFile.build(**to_h) + + def clean_location + return if location.nil? + + if location.starts_with? "/" + CGI.unescape(location) + else + CGI.unescape("/#{location}") + end + end + + def self.from_id(file_id) + new(id: file_id, status: "OK", status_code: 200) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/results/storage_file_info_contract.rb b/modules/storages/app/common/storages/adapters/results/storage_file_info_contract.rb new file mode 100644 index 00000000000..88085033bc7 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/storage_file_info_contract.rb @@ -0,0 +1,52 @@ +# 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 Adapters + module Results + class StorageFileInfoContract < Dry::Validation::Contract + params(StorageFileContract.schema) do + before(:value_coercer) do |input| + input.to_h.compact + end + + required(:status).filled(:string) + required(:status_code).filled(:integer) + optional(:name).filled(:string) + optional(:last_modified_by_id).filled(:string) + optional(:created_by_name).filled(:string) + optional(:last_modified_by_name).filled(:string) + optional(:owner_name).filled(:string) + optional(:owner_id).filled(:string) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/results/upload_link.rb b/modules/storages/app/common/storages/adapters/results/upload_link.rb new file mode 100644 index 00000000000..f8d51deaa45 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/upload_link.rb @@ -0,0 +1,47 @@ +# 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 Adapters + module Results + # FIXME: UploadDestination or something similar might be more suitable here. + UploadLink = Data.define(:destination, :method) do + private_class_method :new + + def self.build(destination:, method:, contract: UploadLinkContract.new) + contract.call(destination:, method:).to_monad.fmap do |it| + params = it.to_h + new(URI(params[:destination]), params[:method]) + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/adapters/results/upload_link_contract.rb b/modules/storages/app/common/storages/adapters/results/upload_link_contract.rb new file mode 100644 index 00000000000..778e9a89601 --- /dev/null +++ b/modules/storages/app/common/storages/adapters/results/upload_link_contract.rb @@ -0,0 +1,42 @@ +# 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 Adapters + module Results + class UploadLinkContract < Dry::Validation::Contract + params do + required(:destination).filled { uri?(:https) } + required(:method).filled(AdapterTypes::HTTPVerb) + end + end + end + end +end diff --git a/modules/storages/app/common/storages/errors.rb b/modules/storages/app/common/storages/errors.rb index 9f6394d1d46..a77e5f710c7 100644 --- a/modules/storages/app/common/storages/errors.rb +++ b/modules/storages/app/common/storages/errors.rb @@ -32,18 +32,10 @@ module Storages module Errors class BaseError < StandardError; end - class ResolverStandardError < BaseError; end - class PollingRequired < BaseError; end class ConfigurationError < BaseError; end - class MissingContract < ResolverStandardError; end - - class OperationNotSupported < ResolverStandardError; end - - class MissingModel < ResolverStandardError; end - class SubclassResponsibility < BaseError def message "A subclass needs to implement its own version of this method." @@ -51,20 +43,5 @@ module Storages end class IntegrationJobError < BaseError; end - - def self.registry_error_for(key) - case key.split(".") - in [storage, "contracts", model] - MissingContract.new("No #{model} contract defined for provider: #{storage.camelize}") - in [storage, "commands" | "queries" => type, operation] - OperationNotSupported.new( - "#{type.singularize.capitalize} #{operation} not supported by provider: #{storage.camelize}" - ) - in [storage, "models", object] - MissingModel.new("Model #{object} not registered for provider: #{storage.camelize}") - else - ResolverStandardError.new("Cannot resolve key #{key}.") - end - end end end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/ampf_configuration_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/ampf_configuration_validator.rb deleted file mode 100644 index 03e81fb6c66..00000000000 --- a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/ampf_configuration_validator.rb +++ /dev/null @@ -1,141 +0,0 @@ -# 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 Peripherals - module ConnectionValidators - module Nextcloud - class AmpfConfigurationValidator < BaseValidatorGroup - using ServiceResultRefinements - - def self.key = :ampf_configuration - - private - - def validate - register_checks( - :group_folder_app, :files_request, :userless_access, :group_folder_presence, :group_folder_contents - ) - - group_folder_app_checks - files_request_failed_with_unknown_error - userless_access_denied - group_folder_not_found - with_unexpected_content - end - - def userless_access_denied - if files.result == :unauthorized - fail_check(:userless_access, :nc_userless_access_denied) - else - pass_check(:userless_access) - end - end - - def group_folder_app_checks - required_version = SemanticVersion.parse( - nextcloud_dependencies.dig("dependencies", "group_folders_app", "min_version") - ) - - capabilities = Registry["nextcloud.queries.capabilities"].call(storage: @storage, auth_strategy: noop).result - dependency = I18n.t("storages.dependencies.nextcloud.group_folders_app") - - if capabilities.group_folder_disabled? - fail_check(:group_folder_app, :nc_dependency_missing, context: { dependency: }) - elsif capabilities.group_folder_version < required_version - fail_check(:group_folder_app, :nc_dependency_version_mismatch, context: { dependency: }) - else - pass_check(:group_folder_app) - end - end - - def group_folder_not_found - if files.result == :not_found - fail_check(:group_folder_presence, :nc_group_folder_not_found) - else - pass_check(:group_folder_presence) - end - end - - def files_request_failed_with_unknown_error - if files.result == :error - error "Connection validation failed with unknown error:\n" \ - "\tstorage: ##{@storage.id} #{@storage.name}\n" \ - "\trequest: Group folder content\n" \ - "\tstatus: #{files.result}\n" \ - "\tresponse: #{files.error_payload}" - - fail_check(:files_request, :unknown_error) - else - pass_check(:files_request) - end - end - - def with_unexpected_content - unexpected_files = files.result.files.reject { managed_project_folder_ids.include?(it.id) } - return pass_check(:group_folder_contents) if unexpected_files.empty? - - log_extraneous_files(unexpected_files) - warn_check(:group_folder_contents, :nc_unexpected_content) - end - - def log_extraneous_files(unexpected_files) - file_representation = unexpected_files.map do |file| - "Name: #{file.name}, ID: #{file.id}, Location: #{file.location}" - end - - warn "Unexpected files/folder found in group folder:\n\t#{file_representation.join("\n\t")}" - end - - def auth_strategy = Registry["nextcloud.authentication.userless"].call - - def managed_project_folder_ids - @managed_project_folder_ids ||= ProjectStorage.automatic.where(storage: @storage) - .pluck(:project_folder_id).to_set - end - - def files - @files ||= Peripherals::Registry - .resolve("#{@storage}.queries.files") - .call(storage: @storage, auth_strategy:, folder: ParentFolder.new(@storage.group_folder)) - end - - def noop = StorageInteraction::AuthenticationStrategies::Noop.strategy - - def nextcloud_dependencies - @nextcloud_dependencies ||= YAML.load_file(path_to_config).deep_stringify_keys - end - - def path_to_config = Rails.root.join("modules/storages/config/nextcloud_dependencies.yml") - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/authentication_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/authentication_validator.rb deleted file mode 100644 index 2b94ed781f3..00000000000 --- a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/authentication_validator.rb +++ /dev/null @@ -1,138 +0,0 @@ -# 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 Peripherals - module ConnectionValidators - module Nextcloud - class AuthenticationValidator < BaseValidatorGroup - def self.key = :authentication - - def initialize(storage) - super - @user = User.current - end - - private - - def validate - @storage.authenticate_via_idp? ? validate_sso : validate_oauth - end - - def validate_oauth - register_checks(:existing_token, :user_bound_request) - - oauth_token - user_bound_request - end - - def oauth_token - if OAuthClientToken.for_user_and_client(@user, @storage.oauth_client).exists? - pass_check(:existing_token) - else - warn_check(:existing_token, :nc_oauth_token_missing, halt_validation: true) - end - end - - def user_bound_request - Registry["nextcloud.queries.user"].call(storage: @storage, auth_strategy:).on_failure do - fail_check(:user_bound_request, :"nc_oauth_request_#{it.result}") - end - - pass_check(:user_bound_request) - end - - def auth_strategy = Registry["nextcloud.authentication.user_bound"].call(storage: @storage, user: @user) - - def validate_sso - register_checks( - :non_provisioned_user, - :provisioned_user_provider, - :token_negotiable, - :user_bound_request, - :offline_access - ) - - non_provisioned_user - non_oidc_provisioned_user - token_negotiable - user_bound_request - offline_access - end - - def non_provisioned_user - if @user.identity_url.present? - pass_check(:non_provisioned_user) - else - warn_check(:non_provisioned_user, :oidc_non_provisioned_user, halt_validation: true) - end - end - - def non_oidc_provisioned_user - if @user.authentication_provider.is_a?(OpenIDConnect::Provider) - pass_check(:provisioned_user_provider) - else - warn_check(:provisioned_user_provider, :oidc_non_oidc_user, halt_validation: true) - end - end - - def token_negotiable - service = OpenIDConnect::UserTokens::FetchService.new(user: @user, exchange_scope: @storage.token_exchange_scope) - - result = service.access_token_for(audience: @storage.audience) - return pass_check(:token_negotiable) if result.success? - - error_code = - case result.failure - in { code: /token_exchange/ | :unable_to_exchange_token } - :oidc_token_exchange_failed - in { code: /token_refresh/ } - :oidc_token_refresh_failed - in { code: :no_token_for_audience } - :oidc_token_acquisition_failed - else - :unknown_error - end - - fail_check(:token_negotiable, error_code) - end - - def offline_access - if @user.authentication_provider.scopes.include?("offline_access") - pass_check(:offline_access) - else - warn_check(:offline_access, :offline_access_scope_missing) - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/storage_configuration_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/storage_configuration_validator.rb deleted file mode 100644 index c7da540791a..00000000000 --- a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/storage_configuration_validator.rb +++ /dev/null @@ -1,114 +0,0 @@ -# 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 Peripherals - module ConnectionValidators - module Nextcloud - class StorageConfigurationValidator < BaseValidatorGroup - def self.key = :base_configuration - - private - - def validate - register_checks(:storage_configured, :host_url_accessible, :capabilities_request, - :dependencies_check, :dependencies_versions) - - storage_configuration_status - host_url_not_found - capabilities_request_status - missing_dependencies - version_mismatch - end - - def storage_configuration_status - if @storage.configured? - pass_check(:storage_configured) - else - fail_check(:storage_configured, :not_configured) - end - end - - def capabilities_request_status - if capabilities.failure? && capabilities.result != :not_found - fail_check(:capabilities_request, :unknown_error) - else - pass_check(:capabilities_request) - end - end - - def version_mismatch - min_app_version = SemanticVersion.parse(nextcloud_dependencies.dig("dependencies", "integration_app", "min_version")) - capabilities_result = capabilities.result - dependency = I18n.t("storages.dependencies.nextcloud.integration_app") - - if capabilities_result.app_version < min_app_version - fail_check(:dependencies_versions, :nc_dependency_version_mismatch, context: { dependency: }) - else - pass_check(:dependencies_versions) - end - end - - def missing_dependencies - capabilities_result = capabilities.result - dependency = I18n.t("storages.dependencies.nextcloud.integration_app") - - if capabilities_result.app_disabled? - fail_check(:dependencies_check, :nc_dependency_missing, context: { dependency: }) - else - pass_check(:dependencies_check) - end - end - - def host_url_not_found - if capabilities.result == :not_found - fail_check(:host_url_accessible, :nc_host_not_found) - else - pass_check(:host_url_accessible) - end - end - - def noop = StorageInteraction::AuthenticationStrategies::Noop.strategy - - def capabilities - @capabilities ||= Peripherals::Registry.resolve("#{@storage}.queries.capabilities") - .call(storage: @storage, auth_strategy: noop) - end - - def nextcloud_dependencies - @nextcloud_dependencies ||= YAML.load_file(path_to_config).deep_stringify_keys! - end - - def path_to_config = Rails.root.join("modules/storages/config/nextcloud_dependencies.yml") - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud_validator.rb deleted file mode 100644 index b09e3a6aa6d..00000000000 --- a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud_validator.rb +++ /dev/null @@ -1,48 +0,0 @@ -# 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 Peripherals - module ConnectionValidators - class NextcloudValidator < BaseConnectionValidator - register_group Nextcloud::StorageConfigurationValidator - register_group Nextcloud::AuthenticationValidator, - precondition: ->(_, result) do - result.group(Nextcloud::StorageConfigurationValidator.key).non_failure? - end - register_group Nextcloud::AmpfConfigurationValidator, - precondition: ->(storage, result) do - result.group(Nextcloud::StorageConfigurationValidator.key).non_failure? && - storage.automatic_management_enabled? - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator.rb deleted file mode 100644 index 5ae677a143c..00000000000 --- a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator.rb +++ /dev/null @@ -1,107 +0,0 @@ -# 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 Peripherals - module ConnectionValidators - module OneDrive - class AmpfConfigurationValidator < BaseValidatorGroup - TEST_FOLDER_NAME = "OpenProjectConnectionValidationFolder" - - def self.key = :ampf_configuration - - private - - def validate - register_checks :client_folder_creation, :client_folder_removal, :drive_contents - - client_permissions - unexpected_content - end - - def unexpected_content - unexpected_files = files_query - .on_failure { fail_check(:drive_contents, :unknown_error) } - .result.files.reject { managed_project_folder_ids.include?(it.id) } - - if unexpected_files.empty? - pass_check(:drive_contents) - else - log_extraneous_files(unexpected_files) - warn_check(:drive_contents, :od_unexpected_content) - end - end - - # Testing setting permissions and checking permission inheritance would be great - # but there are some challenges to it. We need to figure out a good way to go about this - # 2025-04-08 @mereghost - def client_permissions - folder = create_folder.result - delete_folder(folder) - end - - def delete_folder(folder) - Registry["one_drive.commands.delete_folder"] - .call(storage: @storage, auth_strategy:, location: folder.id) - .on_failure { fail_check(:client_folder_removal, :od_client_cant_delete_folder) } - .on_success { pass_check(:client_folder_removal) } - end - - def create_folder - Registry["one_drive.commands.create_folder"] - .call(storage: @storage, auth_strategy:, folder_name: TEST_FOLDER_NAME, parent_location: ParentFolder.root) - .on_success { pass_check(:client_folder_creation) } - .on_failure do - code = it.result == :already_exists ? :od_test_folder_exists : :od_client_write_permission_missing - fail_check(:client_folder_creation, code, context: { folder_name: TEST_FOLDER_NAME }) - end - end - - def log_extraneous_files(unexpected_files) - file_representation = unexpected_files.map do |file| - "Name: #{file.name}, ID: #{file.id}, Location: #{file.location}" - end - - warn "Unexpected files/folder found in group folder:\n\t#{file_representation.join("\n\t")}" - end - - def managed_project_folder_ids - @managed_project_folder_ids ||= ProjectStorage.automatic.where(storage: @storage) - .pluck(:project_folder_id).to_set - end - - def files_query = Registry["one_drive.queries.files"].call(storage: @storage, auth_strategy:, folder: ParentFolder.root) - - def auth_strategy = Registry["one_drive.authentication.userless"].call - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator.rb deleted file mode 100644 index a5a07fe1ec2..00000000000 --- a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator.rb +++ /dev/null @@ -1,148 +0,0 @@ -# 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 Peripherals - module ConnectionValidators - module OneDrive - class StorageConfigurationValidator < BaseValidatorGroup - using ServiceResultRefinements - - def self.key = :base_configuration - - private - - def validate - register_checks :storage_configured, - :diagnostic_request, - :tenant_id, - :client_secret, - :client_id, - :drive_id_format, - :drive_id_exists - - storage_configuration_status - diagnostic_request - check_tenant_id - check_client_secret - check_client_id - malformed_drive_id - drive_not_found - end - - def malformed_drive_id - return pass_check(:drive_id_format) if query_result.success? - - if error_payload.dig(:error, :code) == "invalidRequest" - fail_check(:drive_id_format, :od_drive_id_invalid) - else - pass_check(:drive_id_format) - end - end - - def drive_not_found - if query_result.result == :not_found - fail_check(:drive_id_exists, :od_drive_id_not_found) - else - pass_check(:drive_id_exists) - end - end - - def check_tenant_id - return pass_check(:tenant_id) if query_result.success? - - tenant_id_regex = /tenant (?:identifier )?'#{@storage.tenant_id}' (?:not found|is neither)/i - - if error_payload[:error] == "invalid_request" && error_payload[:error_description].match?(tenant_id_regex) - fail_check(:tenant_id, :od_tenant_id_invalid) - else - pass_check(:tenant_id) - end - end - - def check_client_id - return pass_check(:client_id) if query_result.success? - - if error_payload[:error] == "unauthorized_client" - fail_check(:client_id, :client_id_invalid) - else - pass_check(:client_id) - end - end - - def check_client_secret - return pass_check(:client_secret) if query_result.success? - - if error_payload[:error] == "invalid_client" - fail_check(:client_secret, :client_secret_invalid) - else - pass_check(:client_secret) - end - end - - def diagnostic_request - if query_result.result == :error - error "Connection validation failed with unknown error:\n" \ - "\tstorage: ##{@storage.id} #{@storage.name}\n" \ - "\tstatus: #{query_result.result}\n" \ - "\tresponse: #{query_result.error_payload}" - - fail_check(:diagnostic_request, :unknown_error) - else - pass_check :diagnostic_request - end - end - - def storage_configuration_status - if @storage.configured? - pass_check(:storage_configured) - else - fail_check(:storage_configured, :not_configured) - end - end - - def query_result - @query_result ||= Registry.resolve("#{@storage}.queries.files") - .call(storage: @storage, auth_strategy:, folder: ParentFolder.root) - end - - def auth_strategy = Registry.resolve("one_drive.authentication.userless").call - - def error_payload - return {} if query_result.success? - return query_result.error_payload if query_result.error_payload.is_a?(Hash) - - @error_payload ||= MultiJson.load(query_result.error_payload, symbolize_keys: true) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive_validator.rb deleted file mode 100644 index c517610b62e..00000000000 --- a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive_validator.rb +++ /dev/null @@ -1,45 +0,0 @@ -# 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 Peripherals - module ConnectionValidators - class OneDriveValidator < BaseConnectionValidator - register_group OneDrive::StorageConfigurationValidator - register_group OneDrive::AuthenticationValidator, - precondition: ->(_, result) { result.group(:base_configuration).non_failure? } - register_group OneDrive::AmpfConfigurationValidator, - precondition: ->(storage, result) { - result.group(:base_configuration).non_failure? && storage.automatic_management_enabled? - } - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb b/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb deleted file mode 100644 index 441ffe74042..00000000000 --- a/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb +++ /dev/null @@ -1,104 +0,0 @@ -# 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 Peripherals - NextcloudRegistry = Dry::Container::Namespace.new("nextcloud") do - namespace("queries") do - register(:user, StorageInteraction::Nextcloud::UserQuery) - register(:capabilities, StorageInteraction::Nextcloud::CapabilitiesQuery) - register(:download_link, StorageInteraction::Nextcloud::DownloadLinkQuery) - register(:file_info, StorageInteraction::Nextcloud::FileInfoQuery) - register(:files_info, StorageInteraction::Nextcloud::FilesInfoQuery) - register(:files, StorageInteraction::Nextcloud::FilesQuery) - register(:file_path_to_id_map, StorageInteraction::Nextcloud::FilePathToIdMapQuery) - register(:propfind, StorageInteraction::Nextcloud::Internal::PropfindQuery) - register(:group_users, StorageInteraction::Nextcloud::GroupUsersQuery) - register(:upload_link, StorageInteraction::Nextcloud::UploadLinkQuery) - register(:open_file_link, StorageInteraction::Nextcloud::OpenFileLinkQuery) - register(:open_storage, StorageInteraction::Nextcloud::OpenStorageQuery) - end - - namespace("commands") do - register(:add_user_to_group, StorageInteraction::Nextcloud::AddUserToGroupCommand) - register(:copy_template_folder, StorageInteraction::Nextcloud::CopyTemplateFolderCommand) - register(:create_folder, StorageInteraction::Nextcloud::CreateFolderCommand) - register(:delete_entity, StorageInteraction::Nextcloud::Internal::DeleteEntityCommand) - register(:delete_folder, StorageInteraction::Nextcloud::DeleteFolderCommand) - register(:remove_user_from_group, StorageInteraction::Nextcloud::RemoveUserFromGroupCommand) - register(:rename_file, StorageInteraction::Nextcloud::RenameFileCommand) - register(:set_permissions, StorageInteraction::Nextcloud::SetPermissionsCommand) - end - - namespace("components") do - namespace("forms") do - register(:automatically_managed_folders, Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent) - register(:general_information, Admin::Forms::GeneralInfoFormComponent) - register(:storage_audience, Admin::Forms::StorageAudienceFormComponent) - register(:oauth_application, Admin::OAuthApplicationInfoCopyComponent) - register(:oauth_client, Admin::Forms::OAuthClientFormComponent) - end - - register(:setup_wizard, NextcloudStorageWizard) - - register(:automatically_managed_folders, Admin::AutomaticallyManagedProjectFoldersInfoComponent) - register(:general_information, Admin::GeneralInfoComponent) - register(:storage_audience, Admin::StorageAudienceInfoComponent) - register(:oauth_application, Admin::OAuthApplicationInfoComponent) - register(:oauth_client, Admin::OAuthClientInfoComponent) - end - - namespace("contracts") do - register(:storage, Storages::NextcloudContract) - register(:general_information, Storages::NextcloudGeneralInformationContract) - register(:storage_audience, Storages::NextcloudAudienceContract) - end - - namespace("models") do - register(:managed_folder_identifier, ManagedFolderIdentifier::Nextcloud) - end - - namespace("services") do - register(:folder_create, ::Storages::NextcloudManagedFolderCreateService) - register(:folder_permissions, ::Storages::NextcloudManagedFolderPermissionsService) - end - - namespace("validators") do - register(:connection, ConnectionValidators::NextcloudValidator) - end - - namespace("authentication") do - register(:userless, StorageInteraction::AuthenticationStrategies::NextcloudStrategies::UserLess, call: false) - register(:user_bound, StorageInteraction::AuthenticationStrategies::NextcloudStrategies::UserBound) - register(:specific_bearer_token, StorageInteraction::AuthenticationStrategies::NextcloudStrategies::SpecificBearerToken) - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/nextcloud_storage_wizard.rb b/modules/storages/app/common/storages/peripherals/nextcloud_storage_wizard.rb deleted file mode 100644 index 22c33b0e9d7..00000000000 --- a/modules/storages/app/common/storages/peripherals/nextcloud_storage_wizard.rb +++ /dev/null @@ -1,75 +0,0 @@ -# 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 Peripherals - class NextcloudStorageWizard < Wizard - step :general_information, completed_if: ->(storage) { storage.host.present? && storage.name.present? } - - # OAuth 2.0 SSO - - step :storage_audience, - section: :oauth_configuration, - if: ->(storage) { storage.authenticate_via_idp? }, - completed_if: ->(storage) { storage.storage_audience.present? } - - # Two-Way OAuth 2.0 - - step :oauth_application, - section: :oauth_configuration, - if: ->(storage) { storage.authenticate_via_storage? }, - completed_if: ->(storage) { storage.oauth_application.present? }, - preparation: :prepare_oauth_application - - step :oauth_client, - section: :oauth_configuration, - if: ->(storage) { storage.authenticate_via_storage? }, - completed_if: ->(storage) { storage.oauth_client.present? }, - preparation: ->(storage) { storage.build_oauth_client } - - step :automatically_managed_folders, - completed_if: ->(storage) { !storage.automatic_management_unspecified? }, - preparation: :prepare_storage_for_automatic_management_form - - private - - def prepare_oauth_application(storage) - create_result = ::Storages::OAuthApplications::CreateService.new(storage:, user:).call - storage.oauth_application = create_result.result if create_result.success? - end - - def prepare_storage_for_automatic_management_form(storage) - ::Storages::Storages::SetProviderFieldsAttributesService - .new(user:, model: storage, contract_class: EmptyContract) - .call - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/oauth_configurations/nextcloud_configuration.rb b/modules/storages/app/common/storages/peripherals/oauth_configurations/nextcloud_configuration.rb deleted file mode 100644 index 9ae5e6a8fb1..00000000000 --- a/modules/storages/app/common/storages/peripherals/oauth_configurations/nextcloud_configuration.rb +++ /dev/null @@ -1,80 +0,0 @@ -# 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 Peripherals - module OAuthConfigurations - class NextcloudConfiguration < ConfigurationInterface - Util = StorageInteraction::Nextcloud::Util - - attr_reader :oauth_client - - # rubocop:disable Lint/MissingSuper - def initialize(storage) - @storage = storage - - raise(ArgumentError, "Storage must have configured OAuth client credentials") if storage.oauth_client.blank? - - @oauth_client = storage.oauth_client.freeze - end - - # rubocop:enable Lint/MissingSuper - - def to_httpx_oauth_config - StorageInteraction::AuthenticationStrategies::OAuthConfiguration.new( - client_id: @oauth_client.client_id, - client_secret: @oauth_client.client_secret, - issuer: URI(UrlBuilder.url(@storage.uri, "/index.php/apps/oauth2/api/v1")).normalize, - scope: [] - ) - end - - def scope - [] - end - - def basic_rack_oauth_client - uri = @storage.uri - - Rack::OAuth2::Client.new( - identifier: @oauth_client.client_id, - secret: @oauth_client.client_secret, - redirect_uri: @oauth_client.redirect_uri, - scheme: uri.scheme, - host: uri.host, - port: uri.port, - authorization_endpoint: UrlBuilder.path(uri.path, "/index.php/apps/oauth2/authorize"), - token_endpoint: UrlBuilder.path(uri.path, "/index.php/apps/oauth2/api/v1/token") - ) - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/oauth_configurations/one_drive_configuration.rb b/modules/storages/app/common/storages/peripherals/oauth_configurations/one_drive_configuration.rb deleted file mode 100644 index ef77ab91b07..00000000000 --- a/modules/storages/app/common/storages/peripherals/oauth_configurations/one_drive_configuration.rb +++ /dev/null @@ -1,82 +0,0 @@ -# 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 Peripherals - module OAuthConfigurations - class OneDriveConfiguration < ConfigurationInterface - Util = StorageInteraction::OneDrive::Util - - attr_reader :oauth_client - - # rubocop:disable Lint/MissingSuper - def initialize(storage) - @storage = storage - - raise(ArgumentError, "Storage must have configured OAuth client credentials") if storage.oauth_client.blank? - - @oauth_client = storage.oauth_client.freeze - - raise(ArgumentError, "Storage must have a configured tenant id") if storage.tenant_id.blank? - - @oauth_uri = URI("https://login.microsoftonline.com/#{@storage.tenant_id}/oauth2/v2.0").normalize - end - - # rubocop:enable Lint/MissingSuper - - def to_httpx_oauth_config - StorageInteraction::AuthenticationStrategies::OAuthConfiguration.new( - client_id: @oauth_client.client_id, - client_secret: @oauth_client.client_secret, - issuer: @oauth_uri, - scope: - ) - end - - def scope - %w[https://graph.microsoft.com/.default offline_access] - end - - def basic_rack_oauth_client - Rack::OAuth2::Client.new( - identifier: @oauth_client.client_id, - redirect_uri: @oauth_client.redirect_uri, - secret: @oauth_client.client_secret, - scheme: @oauth_uri.scheme, - host: @oauth_uri.host, - port: @oauth_uri.port, - authorization_endpoint: "#{@oauth_uri.path}/authorize", - token_endpoint: "#{@oauth_uri.path}/token" - ) - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/one_drive_registry.rb b/modules/storages/app/common/storages/peripherals/one_drive_registry.rb deleted file mode 100644 index b34bb5e8bd1..00000000000 --- a/modules/storages/app/common/storages/peripherals/one_drive_registry.rb +++ /dev/null @@ -1,95 +0,0 @@ -# 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 Peripherals - OneDriveRegistry = Dry::Container::Namespace.new("one_drive") do - namespace("queries") do - register(:user, StorageInteraction::OneDrive::UserQuery) - register(:download_link, StorageInteraction::OneDrive::DownloadLinkQuery) - register(:files, StorageInteraction::OneDrive::FilesQuery) - register(:file_info, StorageInteraction::OneDrive::FileInfoQuery) - register(:files_info, StorageInteraction::OneDrive::FilesInfoQuery) - register(:open_file_link, StorageInteraction::OneDrive::OpenFileLinkQuery) - register(:file_path_to_id_map, StorageInteraction::OneDrive::FilePathToIdMapQuery) - register(:open_storage, StorageInteraction::OneDrive::OpenStorageQuery) - register(:upload_link, StorageInteraction::OneDrive::UploadLinkQuery) - end - - namespace("commands") do - register(:copy_template_folder, StorageInteraction::OneDrive::CopyTemplateFolderCommand) - register(:create_folder, StorageInteraction::OneDrive::CreateFolderCommand) - register(:delete_folder, StorageInteraction::OneDrive::DeleteFolderCommand) - register(:rename_file, StorageInteraction::OneDrive::RenameFileCommand) - register(:set_permissions, StorageInteraction::OneDrive::SetPermissionsCommand) - end - - namespace("components") do - namespace("forms") do - register(:access_management, Admin::Forms::AccessManagementFormComponent) - register(:general_information, Admin::Forms::GeneralInfoFormComponent) - register(:oauth_client, Admin::Forms::OAuthClientFormComponent) - register(:redirect_uri, Admin::Forms::RedirectUriFormComponent) - end - - register(:setup_wizard, OneDriveStorageWizard) - - register(:access_management, Admin::AccessManagementComponent) - register(:general_information, Admin::GeneralInfoComponent) - register(:oauth_client, Admin::OAuthClientInfoComponent) - register(:redirect_uri, Admin::RedirectUriComponent) - end - - namespace("contracts") do - register(:storage, Storages::OneDriveContract) - register(:general_information, Storages::OneDriveContract) - end - - namespace("models") do - register(:managed_folder_identifier, ManagedFolderIdentifier::OneDrive) - end - - namespace("services") do - register(:folder_create, ::Storages::OneDriveManagedFolderCreateService) - register(:folder_permissions, ::Storages::OneDriveManagedFolderPermissionsService) - end - - namespace("validators") do - register(:connection, ConnectionValidators::OneDriveValidator) - end - - namespace("authentication") do - register(:userless, StorageInteraction::AuthenticationStrategies::OneDriveStrategies::UserLess, call: false) - register(:user_bound, StorageInteraction::AuthenticationStrategies::OneDriveStrategies::UserBound) - register(:specific_bearer_token, StorageInteraction::AuthenticationStrategies::OneDriveStrategies::SpecificBearerToken) - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/one_drive_storage_wizard.rb b/modules/storages/app/common/storages/peripherals/one_drive_storage_wizard.rb deleted file mode 100644 index 242aaeb81c0..00000000000 --- a/modules/storages/app/common/storages/peripherals/one_drive_storage_wizard.rb +++ /dev/null @@ -1,75 +0,0 @@ -# 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 Peripherals - class OneDriveStorageWizard < Wizard - step :general_information, completed_if: ->(storage) { storage.name.present? } - - step :access_management, - section: :access_management_section, - completed_if: ->(storage) { !storage.automatic_management_unspecified? }, - preparation: :prepare_storage_for_access_management_form - - step :oauth_client, - section: :oauth_configuration, - completed_if: ->(storage) { storage.oauth_client.present? }, - preparation: ->(storage) { storage.build_oauth_client } - - step :redirect_uri, - section: :oauth_configuration, - completed_if: ->(storage) { - # Working around the fact that there is nothing changed on the storage after showing - # the redirect url. The redirect URL step only exists to show the oauth client's redirect - # URL to the user right after the client was created. - storage.oauth_client&.persisted? && storage.oauth_client.created_at < 10.seconds.ago - } - - private - - def prepare_storage_for_access_management_form(storage) - ::Storages::Storages::SetProviderFieldsAttributesService - .new(user:, model: storage, contract_class: EmptyContract) - .call - end - - def prepare_oauth_application(storage) - persist_service_result = ::Storages::OAuthApplications::CreateService.new(storage:, user:).call - storage.oauth_application = persist_service_result.result if persist_service_result.success? - end - - def prepare_storage_for_automatic_management_form(storage) - ::Storages::Storages::SetProviderFieldsAttributesService - .new(user:, model: storage, contract_class: EmptyContract) - .call - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_error_helper.rb b/modules/storages/app/common/storages/peripherals/storage_error_helper.rb index aa3336c80f5..54b0f1451a6 100644 --- a/modules/storages/app/common/storages/peripherals/storage_error_helper.rb +++ b/modules/storages/app/common/storages/peripherals/storage_error_helper.rb @@ -38,8 +38,8 @@ module Storages::Peripherals end def handle_base_errors(errors) - base_errors = errors.symbols_for(:base) - message = errors.full_messages_for(:base)&.first + base_errors = errors.symbols_for(:base).to_set + message = error_message_for(errors) if base_errors.include? :not_found fail API::Errors::OutboundRequestNotFound.new(message) @@ -54,20 +54,28 @@ module Storages::Peripherals end end + def error_message_for(error, attr = :base) + error.full_messages_for(attr)&.first + rescue I18n::MissingTranslationData + nil + end + def raise_error(error) Rails.logger.error(error) + # FIXME: messages were removed we need to deal with it - 2025-04-14 @mereghost + case error.code when :not_found raise API::Errors::OutboundRequestNotFound.new when :bad_request - raise API::Errors::BadRequest.new(error.log_message) + raise API::Errors::BadRequest.new(error.code) when :forbidden raise API::Errors::OutboundRequestForbidden.new when :missing_ee_token_for_one_drive raise API::Errors::EnterpriseTokenMissing.new else - raise API::Errors::InternalError.new(error.log_message) + raise API::Errors::InternalError.new(error.code) end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication.rb deleted file mode 100644 index 549131aebae..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication.rb +++ /dev/null @@ -1,81 +0,0 @@ -# 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 Peripherals - module StorageInteraction - class Authentication - using ServiceResultRefinements - - def self.[](strategy) - case strategy.key - when :noop - AuthenticationStrategies::Noop.new - when :failure - AuthenticationStrategies::Failure.new - when :basic_auth - AuthenticationStrategies::BasicAuth.new - when :bearer_token - AuthenticationStrategies::SpecificBearerToken.new(strategy.token) - when :sso_user_token - AuthenticationStrategies::SsoUserToken.new(strategy.user) - when :oauth_user_token - AuthenticationStrategies::OAuthUserToken.new(strategy.user) - when :oauth_client_credentials - AuthenticationStrategies::OAuthClientCredentials.new(strategy.use_cache) - else - raise "Invalid authentication strategy '#{strategy}'" - end - end - - # Checks for the current authorization state of a user on a specific file storage. - # Returns one of three results: - # - :connected If requests can be made to the storage in the user's name - # - :failed_authorization If the token to request the storage in the user's name was rejected - # - :not_connected if the user still needs to establish a connection to the storage - # - :error If an unexpected error occurred - def self.authorization_state(storage:, user:) - selector = AuthenticationMethodSelector.new(storage:, user:) - return :not_connected if RemoteIdentity.where(integration: storage, user:).none? && selector.storage_oauth? - - auth_strategy = Registry.resolve("#{storage}.authentication.user_bound").call(user:, storage:) - - Registry - .resolve("#{storage}.queries.user") - .call(storage:, auth_strategy:) - .match( - on_success: ->(*) { :connected }, - on_failure: ->(error) { error.code == :unauthorized ? :failed_authorization : :error } - ) - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_method_selector.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_method_selector.rb deleted file mode 100644 index 1aa76870b24..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_method_selector.rb +++ /dev/null @@ -1,68 +0,0 @@ -# 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 Peripherals - module StorageInteraction - class AuthenticationMethodSelector - attr_reader :storage, :user - - def initialize(storage:, user:) - @storage = storage - @user = user - end - - def authentication_method - sso_preferred = storage.authenticate_via_idp? && oidc_provider_for(user) - - if sso_preferred - :sso - elsif storage.authenticate_via_storage? - :storage_oauth - end - end - - def sso? - authentication_method == :sso - end - - def storage_oauth? - authentication_method == :storage_oauth - end - - private - - def oidc_provider_for(user) - user.authentication_provider.is_a?(OpenIDConnect::Provider) - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/basic_auth.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/basic_auth.rb deleted file mode 100644 index 905644a599f..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/basic_auth.rb +++ /dev/null @@ -1,60 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module AuthenticationStrategies - class BasicAuth - def self.strategy - Strategy.new(:basic_auth) - end - - def call(storage:, http_options: {}) - username = storage.username - password = storage.password - - return build_failure(storage) if username.blank? || password.blank? - - yield OpenProject.httpx.basic_auth(username, password).with(http_options) - end - - private - - def build_failure(storage) - log_message = "Cannot authenticate storage with basic auth. Password or username not configured." - data = ::Storages::StorageErrorData.new(source: self.class, payload: storage) - Failures::Builder.call(code: :error, log_message:, data:) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/failure.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/failure.rb deleted file mode 100644 index bc0c5fa3f23..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/failure.rb +++ /dev/null @@ -1,52 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module AuthenticationStrategies - class Failure - def self.strategy - Strategy.new(:failure) - end - - # rubocop:disable Lint/UnusedMethodArgument - def call(storage:, http_options: {}) - data = ::Storages::StorageErrorData.new(source: self.class) - log_message = "Authentication was forced to fail. No request executed." - Failures::Builder.call(code: :error, log_message:, data:) - end - - # rubocop:enable Lint/UnusedMethodArgument - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/nextcloud_strategies.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/nextcloud_strategies.rb deleted file mode 100644 index 1ffadb5b40d..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/nextcloud_strategies.rb +++ /dev/null @@ -1,79 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module AuthenticationStrategies - module NextcloudStrategies - SpecificBearerToken = -> do - ::Storages::Peripherals::StorageInteraction::AuthenticationStrategies::SpecificBearerToken.strategy - end - - UserLess = -> do - ::Storages::Peripherals::StorageInteraction::AuthenticationStrategies::BasicAuth.strategy - end - - class UserBound - class << self - include TaggedLogging - - def call(user:, storage:) - with_tagged_logger do - selector = AuthenticationMethodSelector.new(user:, storage:) - - case selector.authentication_method - when :sso - ::Storages::Peripherals::StorageInteraction::AuthenticationStrategies::SsoUserToken - .strategy - .with_user(user) - when :storage_oauth - ::Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken - .strategy - .with_user(user) - else - error "No user-bound authentication strategy applicable for file storage #{storage.id}." - ::Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Failure.strategy - end - end - end - - private - - def oidc_provider_for(user) - user.authentication_provider.is_a?(OpenIDConnect::Provider) - end - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_client_credentials.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_client_credentials.rb deleted file mode 100644 index 4db58e7c65e..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_client_credentials.rb +++ /dev/null @@ -1,120 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module AuthenticationStrategies - class OAuthClientCredentials - def self.strategy - Strategy.new(:oauth_client_credentials) - end - - def initialize(use_cache) - @use_cache = use_cache - end - - def call(storage:, http_options: {}) - config = storage.oauth_configuration.to_httpx_oauth_config - return build_failure(storage) unless config.valid? - - token_cache_key = cache_key(storage) - access_token = @use_cache ? Rails.cache.read(token_cache_key) : nil - - http = build_http_session(access_token, config, http_options) - .on_failure { return it } - .result - - operation_result = yield http - - return operation_result unless @use_cache - - case operation_result - in success: true - write_cache(token_cache_key, http) if access_token.blank? - in failure: true, result: :forbidden - clear_cache(token_cache_key) - else - return operation_result - end - - operation_result - end - - private - - def write_cache(key, httpx_session) - access_token = httpx_session.instance_variable_get(:@options).oauth_session.access_token - Rails.cache.write(key, access_token, expires_in: 50.minutes) - end - - def clear_cache(key) = Rails.cache.delete(key) - - def build_http_session(access_token, config, http_options) - if access_token.present? - http_with_current_token(access_token:, http_options:) - else - http_with_new_token(config:, http_options:) - end - end - - def cache_key(storage) = "storage.#{storage.id}.httpx_access_token" - - def http_with_current_token(access_token:, http_options:) - opts = http_options.deep_merge({ headers: { "Authorization" => "Bearer #{access_token}" } }) - ServiceResult.success(result: OpenProject.httpx.with(opts)) - end - - def http_with_new_token(config:, http_options:) - http = OpenProject.httpx - .oauth_auth(**config.to_h, token_endpoint_auth_method: "client_secret_post") - .with_access_token - .with(http_options) - ServiceResult.success(result: http) - rescue HTTPX::HTTPError => e - Failures::Builder.call(code: :unauthorized, - log_message: "Error while fetching OAuth access token.", - data: Failures::ErrorData.call(response: e.response, source: self.class)) - rescue HTTPX::TimeoutError => e - Failures::Builder.call(code: :unauthorized, - log_message: "Timeout while fetching OAuth token.", - data: Failures::TimeoutErrorData.call(error: e, source: self.class)) - end - - def build_failure(storage) - log_message = "Cannot authenticate storage with client credential oauth flow. Storage not configured." - data = ::Storages::StorageErrorData.new(source: self.class, payload: storage) - Failures::Builder.call(code: :error, log_message:, data:) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_user_token.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_user_token.rb deleted file mode 100644 index 267dbe75dea..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_user_token.rb +++ /dev/null @@ -1,181 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module AuthenticationStrategies - class OAuthUserToken - REFRESH_TOKEN_TIMEOUT_SECONDS = 4 - - def self.mutex_refresh_token_key(token) - "Refreshing OAuth token stored in #{token.class}##{token.id}" - end - - def self.strategy - Strategy.new(:oauth_user_token) - end - - def initialize(user) - @user = user - @retried_after_stale_object_update = false - end - - # rubocop:disable Metrics/AbcSize - def call(storage:, http_options: {}, &) - token = current_token(storage).on_failure { |failure| return failure } - - opts = http_options.deep_merge({ headers: { "Authorization" => "Bearer #{token.result.access_token}" } }) - response_with_current_token = yield OpenProject.httpx.with(opts) - - if response_with_current_token.success? || response_with_current_token.result != :unauthorized - response_with_current_token - else - httpx_oauth_config = storage.oauth_configuration.to_httpx_oauth_config - return build_failure(storage) unless httpx_oauth_config.valid? - - refresh_and_retry(httpx_oauth_config, http_options, token.result, &) - end - rescue ActiveRecord::StaleObjectError => e - raise e if @retried_after_stale_object_update - - @retried_after_stale_object_update = true - Rails.logger.error("#{e.inspect} happend for User ##{@user.id} #{@user.name}") - retry - end - - # rubocop:enable Metrics/AbcSize - - private - - def current_token(storage) - data = ::Storages::StorageErrorData.new(source: self.class) - - if storage.oauth_client.blank? - log_message = "Authorization failed. Storage has no configured oauth client credentials." - return Failures::Builder.call(code: :error, log_message:, data:) - end - - # Uncached block is used here because in case of concurrent update on the second try we need a fresh token. - # Otherwise token ends up in an invalid state which could lead to an undesired token deletion. - current_token = OAuthClientToken.uncached do - OAuthClientToken.find_by(user: @user, oauth_client: storage.oauth_configuration.oauth_client) - end - if current_token.nil? - Failures::Builder.call(code: :unauthorized, - log_message: "Authorization failed. No user access token found.", - data:) - else - ServiceResult.success(result: current_token) - end - end - - # rubocop:disable Metrics/AbcSize - def refresh_and_retry(config, http_options, token, &) - http_session = nil - lock_was_acquired = OpenProject::Mutex.with_advisory_lock(OAuthClientToken, - self.class.mutex_refresh_token_key(token), - timeout_seconds: REFRESH_TOKEN_TIMEOUT_SECONDS) do - http_session = OpenProject.httpx - .oauth_auth(issuer: config.issuer, - client_id: config.client_id, - client_secret: config.client_secret, - scope: config.scope, - refresh_token: token.refresh_token, - token_endpoint_auth_method: "client_secret_post") - .with_access_token - .with(http_options) - if update_refreshed_token(token, http_session) - true - else - return Failures::Builder.call(code: :error, - log_message: "Error while persisting updated access token.", - data: ::Storages::StorageErrorData.new(source: self.class)) - end - rescue HTTPX::HTTPError => e - return handle_http_error_on_refresh(token, e) - rescue HTTPX::TimeoutError => e - return handle_timeout_on_refresh(token, e) - end - - if lock_was_acquired - yield http_session - else - Failures::Builder.call(code: :error, - log_message: "Lock has not been acquired in #{REFRESH_TOKEN_TIMEOUT_SECONDS} seconds. " \ - "Refresh token is being updated at the moment by another thread.", - data: ::Storages::StorageErrorData.new(source: self.class)) - end - end - - # rubocop:enable Metrics/AbcSize - - def update_refreshed_token(token, http_session) - oauth = http_session.instance_variable_get(:@options).oauth_session - access_token = oauth.access_token - refresh_token = oauth.refresh_token - - token.update(access_token:, refresh_token:) - end - - def handle_http_error_on_refresh(token, exception) - log_message = "Error while refreshing OAuth token." - data = Failures::ErrorData.call(response: exception.response, source: self.class) - - Rails.logger.error("#{log_message} - Payload: #{data.payload}") - - # Delete token from database to enforce new user login - token.destroy - - Failures::Builder.call(code: :unauthorized, log_message:, data:) - end - - def handle_timeout_on_refresh(token, exception) - log_message = "Timeout while refreshing OAuth token." - data = Failures::TimeoutErrorData.call(error: exception, source: self.class) - - Rails.logger.error("#{log_message} - Payload: #{data.payload}") - - # Delete token from database to enforce new user login - token.destroy - - Failures::Builder.call(code: :unauthorized, log_message:, data:) - end - - def build_failure(storage) - log_message = "Cannot refresh user token for storage. Storage authentication credentials not configured." - data = ::Storages::StorageErrorData.new(source: self.class, payload: storage) - Failures::Builder.call(code: :error, log_message:, data:) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/one_drive_strategies.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/one_drive_strategies.rb deleted file mode 100644 index 159c9f00f8d..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/one_drive_strategies.rb +++ /dev/null @@ -1,53 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module AuthenticationStrategies - module OneDriveStrategies - SpecificBearerToken = -> do - ::Storages::Peripherals::StorageInteraction::AuthenticationStrategies::SpecificBearerToken.strategy - end - - UserLess = -> do - ::Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy - end - - UserBound = ->(user:, storage:) do # rubocop:disable Lint/UnusedBlockArgument - ::Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken - .strategy - .with_user(user) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/set_permissions.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/set_permissions.rb deleted file mode 100644 index 41aa5f12904..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/set_permissions.rb +++ /dev/null @@ -1,60 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Inputs - # user_permissions - A list of user specific file permissions. - # IMPORTANT: the user ids are considered to be the ids of the remote identities. If user permissions should be - # set via a group, a `group_id` must be provided instead of a `user_id`. - # Example: - # [ - # {user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [ :read_files, - # :write_files, - # :create_files, - # :delete_files, - # :share_files ]}, - # {user_id: "f6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files, :write_files]}, - # {group_id: "fee9cd49-17e2-4430-9235-2060e7372568", permissions: [:read_files]}, - # ] - SetPermissions = Data.define(:file_id, :user_permissions) do - private_class_method :new - - def self.build(file_id:, user_permissions:, contract: SetPermissionsContract.new) - contract.call(file_id:, user_permissions:) - .to_monad - .fmap { |result| new(file_id: result[:file_id], user_permissions: result[:user_permissions]) } - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/add_user_to_group_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/add_user_to_group_command.rb deleted file mode 100644 index 11aa671e300..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/add_user_to_group_command.rb +++ /dev/null @@ -1,109 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class AddUserToGroupCommand - include TaggedLogging - - def self.call(storage:, auth_strategy:, user:, group:) - new(storage).call(auth_strategy:, user:, group:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, user:, group:) - with_tagged_logger do - Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| - url = UrlBuilder.url(@storage.uri, "ocs/v1.php/cloud/users", user, "groups") - info "Adding #{user} to #{group} through #{url}" - - response = http.post(UrlBuilder.url(@storage.uri, "ocs/v1.php/cloud/users", user, "groups"), - form: { "groupid" => group }) - - handle_response(response) - end - end - end - - private - - def http_options - Util.ocs_api_request - end - - def handle_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - handle_success_response(response) - in { status: 405 } - Util.error(:not_allowed, "Outbound request method not allowed", error_data) - in { status: 401 } - Util.error(:not_found, "Outbound request destination not found", error_data) - in { status: 404 } - Util.error(:unauthorized, "Outbound request not authorized", 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 - - def handle_success_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - statuscode = Nokogiri::XML(response.body.to_s).xpath("/ocs/meta/statuscode").text - - case statuscode - when "100" - info "User has been added to the group" - ServiceResult.success - when "101" - Util.error(:error, "No group specified", error_data) - when "102" - Util.error(:group_does_not_exist, "Group does not exist", error_data) - when "103" - Util.error(:user_does_not_exist, "User does not exist", error_data) - when "104" - Util.error(:insufficient_privileges, "Insufficient privileges", error_data) - when "105" - Util.error(:failed_to_add, "Failed to add user to group", error_data) - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/capabilities_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/capabilities_query.rb deleted file mode 100644 index 6466ac2de1b..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/capabilities_query.rb +++ /dev/null @@ -1,122 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class CapabilitiesQuery - include Dry::Monads[:result] - include Dry::Monads::Do.for(:parse_capabilities) - - def self.call(storage:, auth_strategy:) - new(storage).call(auth_strategy:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:) - http_options = Util.ocs_api_request.deep_merge(Util.accept_json) - result = Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| - handle_response(http.get(url)) - end - - to_service_result(result) - end - - private - - def url = UrlBuilder.url(@storage.uri, "/ocs/v2.php/cloud/capabilities") - - def handle_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response.to_s) - - case response - in { status: 200..299 } - json = response.json(symbolize_keys: true) - parse_capabilities(json) - in { status: 404 } - Failure(StorageError.new(code: :not_found, - log_message: "Outbound request destination not found!", - data: error_data)) - else - Failure(StorageError.new(code: :error, - log_message: "Outbound request failed!", - data: error_data)) - end - end - - # rubocop:disable Metrics/AbcSize - def parse_capabilities(json) - capabilities = NextcloudCapabilities.empty - - app_json = json.dig(:ocs, :data, :capabilities, :integration_openproject) - capabilities = capabilities.with(app_enabled?: app_json.present?) - - return Success(capabilities) if app_json.nil? - - app_version = yield version(app_json[:app_version]) - group_folder_enabled = !!app_json[:groupfolders_enabled] - capabilities = capabilities.with(app_version:, group_folder_enabled?: group_folder_enabled) - - return Success(capabilities) unless group_folder_enabled - - group_folder_version = yield version(app_json[:groupfolder_version]) - Success(capabilities.with(group_folder_version:)) - end - - # rubocop:enable Metrics/AbcSize - - def version(str) - failure = Failure(StorageError.new(code: :invalid_version_number, - log_message: "'#{str}' is not a valid version string")) - - return failure if str.nil? - - major, minor, patch = str.split(".").map(&:to_i) - - return failure if major.nil? || minor.nil? || patch.nil? - - Success(SemanticVersion.new(major:, minor:, patch:)) - end - - def to_service_result(result) - result.either( - ->(r) { ServiceResult.success(result: r) }, - ->(f) { ServiceResult.failure(result: f.code, errors: f) } - ) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command.rb deleted file mode 100644 index d36bf190f9d..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command.rb +++ /dev/null @@ -1,157 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class CopyTemplateFolderCommand - include TaggedLogging - - using ServiceResultRefinements - - def self.call(auth_strategy:, storage:, source_path:, destination_path:) - new(storage).call(auth_strategy:, source_path:, destination_path:) - end - - def initialize(storage) - @storage = storage - @data = ResultData::CopyTemplateFolder.new(id: nil, polling_url: nil, requires_polling: false) - end - - def call(auth_strategy:, source_path:, destination_path:) - with_tagged_logger do - Authentication[auth_strategy].call(storage: @storage) do |http| - valid_input_result = validate_inputs(source_path, destination_path).on_failure { return it } - - remote_urls = build_origin_urls(**valid_input_result.result) - - ensure_remote_folder_does_not_exist(http, remote_urls[:destination_url]).on_failure { return it } - - copy_folder(http, **remote_urls).on_failure { return it } - - get_folder_id(auth_strategy, valid_input_result.result[:destination_path]) - end - end - end - - private - - def validate_inputs(source_path, destination_path) - info "Validating #{source_path} and #{destination_path}" - if source_path.blank? || destination_path.blank? - return Util.error(:missing_paths, "Source and destination paths must be present.") - end - - ServiceResult.success(result: { source_path:, destination_path: }) - end - - def build_origin_urls(source_path:, destination_path:) - source_url = UrlBuilder.url(@storage.uri, "remote.php/dav/files", @storage.username, source_path) - destination_url = UrlBuilder.url(@storage.uri, "remote.php/dav/files", @storage.username, destination_path) - - { source_url:, destination_url: } - end - - def ensure_remote_folder_does_not_exist(http, destination_url) - info "Checking if #{destination_url} does not already exists." - response = http.head(destination_url) - - case response - in { status: 200..299 } - ServiceResult.failure(result: :conflict, - errors: Util.storage_error( - response:, code: :conflict, source:, - log_message: "The copy would overwrite an already existing folder" - )) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source:)) - in { status: 404 } - ServiceResult.success - else - ServiceResult.failure(result: :error, - errors: Util.storage_error(response:, code: :error, source:)) - end - end - - def copy_folder(http, source_url:, destination_url:) - info "Copying #{source_url} to #{destination_url}" - handle_response http.request("COPY", - source_url, - headers: { "Destination" => destination_url, "Depth" => "infinity" }) - end - - # rubocop:disable Metrics/AbcSize - def handle_response(response) - case response - in { status: 200..299 } - ServiceResult.success(message: "Folder was successfully copied") - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source:)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source:)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source:, - log_message: "Template folder not found")) - in { status: 409 } - ServiceResult.failure(result: :conflict, - errors: Util.storage_error( - response:, code: :conflict, source:, - log_message: Util.error_text_from_response(response) - )) - else - ServiceResult.failure(result: :error, - errors: Util.storage_error(response:, code: :error, source:)) - end - end - - # rubocop:enable Metrics/AbcSize - - def get_folder_id(auth_strategy, destination_path) - # file_path_to_id_map query returns keys without trailing slashes - # TODO: Harden this with https://community.openproject.org/wp/57850 - sanitized_path = destination_path.chomp("/") - - Registry - .resolve("nextcloud.queries.file_path_to_id_map") - .call(storage: @storage, auth_strategy:, folder: ParentFolder.new(sanitized_path), depth: 0) - .map { |result| @data.with(id: result[sanitized_path].id) } - end - - def source = self.class - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command.rb deleted file mode 100644 index baca7d797c3..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command.rb +++ /dev/null @@ -1,146 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class CreateFolderCommand - include TaggedLogging - using ServiceResultRefinements - - 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:) - with_tagged_logger do - info "Trying to create folder #{folder_name} under #{parent_location} using #{auth_strategy.key}" - origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) - .on_failure { |error| return error } - .result - - path_prefix = UrlBuilder.path(@storage.uri.path, "remote.php/dav/files", origin_user_id) - request_url = UrlBuilder.url(@storage.uri, - "remote.php/dav/files", - origin_user_id, - parent_location.path, - folder_name) - - create_folder_request(auth_strategy, request_url, path_prefix) - end - 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? - - handle_response(http.propfind(request_url, requested_properties)).map do |response| - info "Folder successfully created" - 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( - UrlBuilder.path(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 - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/download_link_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/download_link_query.rb deleted file mode 100644 index 807a1db0587..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/download_link_query.rb +++ /dev/null @@ -1,138 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class DownloadLinkQuery - using ServiceResultRefinements - - def self.call(storage:, auth_strategy:, file_link:) - new(storage).call(auth_strategy:, file_link:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, file_link:) - if file_link.nil? - return failure(code: :error, payload: nil, log_message: "File link can not be nil.") - end - - direct_download_request(auth_strategy:, file_link:) - .bind { |response_body| direct_download_token(body: response_body) } - .map { |download_token| download_link(download_token, file_link.origin_name) } - end - - private - - def http_options - Util.ocs_api_request.deep_merge(Util.accept_json) - end - - def direct_download_request(auth_strategy:, file_link:) - Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| - result = handle_response http.post(UrlBuilder.url(@storage.uri, "/ocs/v2.php/apps/dav/api/v1/direct"), - json: { fileId: file_link.origin_id }) - - result.bind do |resp| - # The nextcloud API returns a successful response with empty body if the authorization is missing or expired - if resp.body.blank? - Util.error(:unauthorized, "Outbound request not authorized!") - else - ServiceResult.success(result: resp.body.to_s) - end - end - end - end - - def handle_response(response) - case response - in { status: 200..299 } - ServiceResult.success(result: response) - in { status: 404 } - failure(code: :not_found, - payload: response.json(symbolize_keys: true), - log_message: "Outbound request destination not found!") - in { status: 401 } - failure(code: :unauthorized, - payload: response.json(symbolize_keys: true), - log_message: "Outbound request not authorized!") - else - failure(code: :error, - payload: response.json(symbolize_keys: true), - log_message: "Outbound request failed with unknown error!") - end - end - - def download_link(token, origin_name) - UrlBuilder.url(@storage.uri, "index.php/apps/integration_openproject/direct", token, origin_name) - end - - def direct_download_token(body:) - token = parse_direct_download_token(body:) - if token.blank? - return Util.error(:error, "Received unexpected json response", body) - end - - ServiceResult.success(result: token) - end - - def parse_direct_download_token(body:) - begin - json = JSON.parse(body) - rescue JSON::ParserError - return nil - end - - direct_download_url = json.dig("ocs", "data", "url") - return nil if direct_download_url.blank? - - path = URI.parse(direct_download_url).path - return nil if path.blank? - - path.split("/").last - end - - def failure(code:, payload:, log_message:) - ServiceResult.failure( - result: code, - errors: StorageError.new(code:, - data: StorageErrorData.new(source: self.class, payload:), - log_message:) - ) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_info_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_info_query.rb deleted file mode 100644 index 2e6f0e27438..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/file_info_query.rb +++ /dev/null @@ -1,149 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class FileInfoQuery - using ServiceResultRefinements - - FILE_INFO_PATH = "ocs/v1.php/apps/integration_openproject/fileinfo" - - def self.call(storage:, auth_strategy:, file_id:) - new(storage).call(auth_strategy:, file_id:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, file_id:) - validation = validate_input(file_id) - return validation if validation.failure? - - http_options = Util.ocs_api_request.deep_merge(Util.accept_json) - Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| - file_info(http, file_id).map(&parse_json) >> handle_failure >> create_storage_file_info - end - end - - private - - def validate_input(file_id) - if file_id.nil? - ServiceResult.failure( - result: :error, - errors: StorageError.new(code: :error, - data: StorageErrorData.new(source: self.class), - log_message: "File ID can not be nil") - ) - else - ServiceResult.success - end - end - - def file_info(http, file_id) - response = http.get(UrlBuilder.url(@storage.uri, FILE_INFO_PATH, file_id)) - error_data = StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - ServiceResult.success(result: response.body) - in { status: 404 } - Util.error(:not_found, "Outbound request destination not found!", error_data) - in { status: 401 } - Util.error(:unauthorized, "Outbound request not authorized!", error_data) - else - Util.error(:error, "Outbound request failed!", error_data) - end - end - - def parse_json - ->(response_body) do - JSON.parse(response_body, object_class: OpenStruct) # rubocop:disable Style/OpenStructUse - end - end - - def handle_failure - ->(response_object) do - error_data = StorageErrorData.new(source: self.class, payload: response_object) - - case response_object.ocs.data.statuscode - when 200..299 - ServiceResult.success(result: response_object) - when 403 - Util.error(:forbidden, "Access to storage file forbidden!", error_data) - when 404 - Util.error(:not_found, "Storage file not found!", error_data) - else - Util.error(:error, "Outbound request failed!", error_data) - end - end - end - - def create_storage_file_info # rubocop:disable Metrics/AbcSize - ->(response_object) do - data = response_object.ocs.data - ServiceResult.success( - result: StorageFileInfo.new( - status: data.status.downcase, - status_code: data.statuscode, - id: data.id.to_s, - name: data.name, - last_modified_at: Time.zone.at(data.mtime), - created_at: Time.zone.at(data.ctime), - mime_type: data.mimetype, - size: data.size, - owner_name: data.owner_name, - owner_id: data.owner_id, - last_modified_by_name: data.modifier_name, - last_modified_by_id: data.modifier_id, - permissions: data.dav_permissions, - location: location(data.path) - ) - ) - end - end - - def location(file_path) - prefix = "files/" - idx = file_path.index(prefix) - return "/" if idx == nil - - idx += prefix.length - 1 - - UrlBuilder.path(file_path[idx..]) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_info_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_info_query.rb deleted file mode 100644 index 1b1b5da045e..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_info_query.rb +++ /dev/null @@ -1,141 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class FilesInfoQuery - include TaggedLogging - using ServiceResultRefinements - - FILES_INFO_PATH = "ocs/v1.php/apps/integration_openproject/filesinfo" - - def self.call(storage:, auth_strategy:, file_ids:) - new(storage).call(auth_strategy:, file_ids:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, file_ids:) - with_tagged_logger do - if file_ids.nil? - return Util.error(:error, "File IDs can not be nil", file_ids) - end - - if file_ids.empty? - return ServiceResult.success(result: []) - end - - info "Retrieving file information for #{file_ids.join(', ')}" - http_options = Util.ocs_api_request.deep_merge(Util.accept_json) - Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| - parsed_response = files_info(http, file_ids).on_failure { return it }.result - create_storage_file_infos(parsed_response) - end - end - end - - private - - def files_info(http, file_ids) - response = http.post(UrlBuilder.url(@storage.uri, FILES_INFO_PATH), json: { fileIds: file_ids }) - error_data = StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - json_response = response.json(symbolize_keys: true) - if json_response.dig(:ocs, :meta, :status) == "ok" - ServiceResult.success(result: json_response) - else - Util.error(:error, "Outbound request failed!", error_data) - end - in { status: 404 } - Util.error(:not_found, "Outbound request destination not found!", error_data) - in { status: 401 } - Util.error(:unauthorized, "Outbound request not authorized!", error_data) - else - Util.error(:error, "Outbound request failed!", error_data) - end - end - - # rubocop:disable Metrics/AbcSize - def create_storage_file_infos(parsed_json) - ServiceResult.success( - result: parsed_json.dig(:ocs, :data)&.map do |(key, value)| - if value[:statuscode] == 200 - StorageFileInfo.new( - status: value[:status], - status_code: value[:statuscode], - id: value[:id], - name: value[:name], - last_modified_at: Time.zone.at(value[:mtime]), - created_at: Time.zone.at(value[:ctime]), - mime_type: value[:mimetype], - size: value[:size], - owner_name: value[:owner_name], - owner_id: value[:owner_id], - last_modified_by_name: value[:modifier_name], - last_modified_by_id: value[:modifier_id], - permissions: value[:dav_permissions], - location: location(value[:path], value[:mimetype]) - ) - else - StorageFileInfo.new( - status: value[:status], - status_code: value[:statuscode], - id: key.to_s.to_i - ) - end - end - ) - end - - # rubocop:enable Metrics/AbcSize - - def location(file_path, mimetype) - prefix = "files/" - idx = file_path.index(prefix) - return "/" if idx == nil - - idx += prefix.length - 1 - # Remove the following when /filesinfo starts responding with a trailing slash for directory paths - # in all supported versions of OpenProjectIntegation Nextcloud App. - file_path << "/" if mimetype == "application/x-op-directory" && file_path[-1] != "/" - - UrlBuilder.path(file_path[idx..]) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_query.rb deleted file mode 100644 index 7dc8a6b6ffe..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_query.rb +++ /dev/null @@ -1,238 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class FilesQuery - def self.call(storage:, auth_strategy:, folder:) - new(storage).call(auth_strategy:, folder:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, folder:) - validate_input_data(folder).on_failure { return it } - - origin_user = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) - .on_failure { return it } - .result - - @location_prefix = CGI.unescape UrlBuilder.path(@storage.uri.path, "remote.php/dav/files", origin_user) - - result = make_request(auth_strategy:, folder:, origin_user:) - storage_files(result) - end - - private - - def validate_input_data(folder) - error_data = StorageErrorData.new(source: self.class) - - if folder.is_a?(ParentFolder) - ServiceResult.success - else - Util.error(:error, "Folder input is not a ParentFolder object.", error_data) - end - end - - def make_request(auth_strategy:, folder:, origin_user:) - Authentication[auth_strategy].call(storage: @storage, - http_options: Util.webdav_request_with_depth(1)) do |http| - response = http.request("PROPFIND", - UrlBuilder.url(@storage.uri, - "remote.php/dav/files", - origin_user, - folder.path), - xml: requested_properties) - handle_response(response) - end - end - - def handle_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - ServiceResult.success(result: response.body) - in { status: 404 } - Util.error(:not_found, "Outbound request destination not found", error_data) - in { status: 401 } - Util.error(:unauthorized, "Outbound request not authorized", error_data) - else - Util.error(:error, "Outbound request failed", error_data) - end - end - - # rubocop:disable Metrics/AbcSize - 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"].getcontenttype - xml["d"].getlastmodified - xml["oc"].permissions - xml["oc"].send(:"owner-display-name") - end - end - end.to_xml - end - - # rubocop:enable Metrics/AbcSize - - def storage_files(response) - response.map do |xml| - parent, *files = Nokogiri::XML(xml) - .xpath("//d:response") - .to_a - .map { |file_element| storage_file(file_element) } - - StorageFiles.new(files, parent, ancestors(parent.location)) - end - end - - def ancestors(parent_location) - path = parent_location.split("/") - return [] if path.count == 0 - - path.take(path.count - 1).reduce([]) do |list, item| - last = list.last - prefix = last.nil? || last.location[-1] != "/" ? "/" : "" - location = "#{last&.location}#{prefix}#{item}" - list.append(forge_ancestor(location)) - end - end - - # The ancestors are simply derived objects from the parents location string. Until we have real information - # from the nextcloud API about the path to the parent, we need to derive name, location and forge an ID. - def forge_ancestor(location) - StorageFile.new(id: Digest::SHA256.hexdigest(location), name: name(location), location:) - end - - def name(location) - location == "/" ? "Root" : CGI.unescape(location.split("/").last) - end - - def storage_file(file_element) - location = location(file_element) - - StorageFile.new( - id: id(file_element), - name: name(location), - size: size(file_element), - mime_type: mime_type(file_element), - last_modified_at: last_modified_at(file_element), - created_by_name: created_by(file_element), - location:, - permissions: permissions(file_element) - ) - end - - def id(element) - element - .xpath(".//oc:fileid") - .map(&:inner_text) - .reject(&:empty?) - .first - end - - def location(element) - texts = element - .xpath("d:href") - .map(&:inner_text) - - return nil if texts.empty? - - element_name = texts.first.delete_prefix(@location_prefix) - - return element_name if element_name == "/" - - element_name.delete_suffix("/") - end - - def size(element) - element - .xpath(".//oc:size") - .map(&:inner_text) - .map { |e| Integer(e) } - .first - end - - def mime_type(element) - element - .xpath(".//d:getcontenttype") - .map(&:inner_text) - .reject(&:empty?) - .first || "application/x-op-directory" - end - - def last_modified_at(element) - element - .xpath(".//d:getlastmodified") - .map { |e| DateTime.parse(e) } - .first - end - - def created_by(element) - element - .xpath(".//oc:owner-display-name") - .map(&:inner_text) - .reject(&:empty?) - .first - end - - def permissions(element) - permissions_string = - element - .xpath(".//oc:permissions") - .map(&:inner_text) - .reject(&:empty?) - .first - - # Nextcloud Dav permissions: - # https://github.com/nextcloud/server/blob/66648011c6bc278ace57230db44fd6d63d67b864/lib/public/Files/DavUtil.php - result = [] - result << :readable if permissions_string.include?("G") - result << :writeable if permissions_string.match?(/W|CK/) - result - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/group_users_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/group_users_query.rb deleted file mode 100644 index fa8b9fef8b8..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/group_users_query.rb +++ /dev/null @@ -1,102 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class GroupUsersQuery - include TaggedLogging - - def self.call(storage:, auth_strategy:, group:) - new(storage).call(auth_strategy:, group:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, group:) - with_tagged_logger do - Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| - url = UrlBuilder.url(@storage.uri, "ocs/v1.php/cloud/groups", group) - info "Requesting user list for group #{group} via url #{url} " - - handle_response(http.get(url)) - end - end - end - - private - - def http_options - Util.ocs_api_request - end - - def handle_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - handle_success_response(response) - in { status: 405 } - Util.error(:not_allowed, "Outbound request method not allowed", error_data) - 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, error_text_from_response(response), error_data) - else - Util.error(:error, "Outbound request failed", error_data) - end - end - - def handle_success_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - xml = Nokogiri::XML(response.body.to_s) - statuscode = xml.xpath("/ocs/meta/statuscode").text - - case statuscode - when "100" - group_users = xml.xpath("/ocs/data/users/element").map(&:text) - info "#{group_users.size} users found" - ServiceResult.success(result: group_users) - when "404" - Util.error(:group_does_not_exist, "Group does not exist", error_data) - else - Util.error(:error, "Unknown response body", error_data) - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command.rb deleted file mode 100644 index d25ddcff0e9..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command.rb +++ /dev/null @@ -1,112 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class RemoveUserFromGroupCommand - include TaggedLogging - - def self.call(storage:, auth_strategy:, user:, group:) - new(storage).call(auth_strategy:, user:, group:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, user:, group:) - with_tagged_logger do - Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| - url = UrlBuilder.url(@storage.uri, "ocs/v1.php/cloud/users", user, "groups") - url += "?groupid=#{CGI.escapeURIComponent(group)}" - - info "Removing #{user} from #{group} through #{url}" - - handle_response(http.delete(url)) - end - end - end - - private - - def http_options - Util.ocs_api_request - end - - def handle_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - handle_success_response(response) - in { status: 405 } - Util.error(:not_allowed, "Outbound request method not allowed", error_data) - 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:disable Metrics/AbcSize - def handle_success_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - - statuscode = Nokogiri::XML(response.body.to_s).xpath("/ocs/meta/statuscode").text - case statuscode - when "100" - info "User has been removed from group" - ServiceResult.success - when "101" - Util.error(:error, "No group specified", error_data) - when "102" - Util.error(:group_does_not_exist, "Group does not exist", error_data) - when "103" - Util.error(:user_does_not_exist, "User does not exist", error_data) - when "104" - Util.error(:insufficient_privileges, "Insufficient privileges", error_data) - when "105" - message = Nokogiri::XML(response.body).xpath("/ocs/meta/message").text - Util.error(:failed_to_remove, message, error_data) - end - end - - # rubocop:enable Metrics/AbcSize - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command.rb deleted file mode 100644 index f0dc8e59037..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command.rb +++ /dev/null @@ -1,121 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class RenameFileCommand - include TaggedLogging - - def self.call(storage:, auth_strategy:, file_id:, name:) - new(storage).call(auth_strategy:, file_id:, name:) - end - - def initialize(storage) - @storage = storage - end - - # rubocop:disable Metrics/AbcSize - def call(auth_strategy:, file_id:, name:) - validate_input_data(file_id:, name:).on_failure { |failure| return failure } - - with_tagged_logger do - info "Validating user remote ID" - origin_user_id = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) - .on_failure { |failure| return failure } - .result - - info "Getting the folder information" - info = FileInfoQuery.call(storage: @storage, auth_strategy:, file_id:) - .on_failure { |failure| return failure } - .result - - info "Renaming the folder #{info.location} to #{name}" - make_request(auth_strategy, origin_user_id, info, name).on_failure { |failure| return failure } - - info "Retrieving updated file info for the #{name} folder" - FileInfoQuery.call(storage: @storage, auth_strategy:, file_id:) - .map { |file_info| Util.storage_file_from_file_info(file_info) } - end - end - # rubocop:enable Metrics/AbcSize - - private - - def make_request(auth_strategy, user, file_info, name) - source_path = UrlBuilder.url(@storage.uri, - "remote.php/dav/files", - user, - CGI.unescape(file_info.location)) - - destination = UrlBuilder.path(@storage.uri.path, - "remote.php/dav/files", - user, - CGI.unescape(target_path(file_info, name))) - - Authentication[auth_strategy].call(storage: @storage) do |http| - handle_response http.request("MOVE", source_path, headers: { "Destination" => destination }) - end - end - - def target_path(info, name) - info.location.gsub(CGI.escapeURIComponent(info.name), CGI.escapeURIComponent(name)) - end - - def validate_input_data(file_id:, name:) - if file_id.blank? || name.blank? - ServiceResult.failure(result: :error, - errors: StorageError.new(code: :error, - data: StorageErrorData.new(source: self.class), - log_message: "file_id or name is blank")) - else - ServiceResult.success - end - end - - def handle_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - case response - in { status: 200..299 } - ServiceResult.success - in { status: 404 } - Util.error(:not_found, "Outbound request destination not found", error_data) - in { status: 401 } - Util.error(:unauthorized, "Outbound request not authorized", error_data) - else - Util.error(:error, "Outbound request failed", error_data) - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/set_permissions_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/set_permissions_command.rb deleted file mode 100644 index 261c2201a65..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/set_permissions_command.rb +++ /dev/null @@ -1,163 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class SetPermissionsCommand - include TaggedLogging - using ServiceResultRefinements - - PERMISSIONS_MAP = { read_files: 1, write_files: 2, create_files: 4, delete_files: 8, share_files: 16 }.freeze - PERMISSIONS_KEYS = OpenProject::Storages::Engine.external_file_permissions - SUCCESS_XPATH = "/d:multistatus/d:response/d:propstat[d:status[text() = 'HTTP/1.1 200 OK']]/d:prop/nc:acl-list" - - # Instantiates the command and executes it. - # - # @param storage [Storage] The storage to interact with. - # @param auth_strategy [AuthenticationStrategy] The authentication strategy to use. - # @param input_data [Inputs::SetPermissions] The data needed for setting permissions, containing the file id - # and the permissions for an array of users. - def self.call(storage:, auth_strategy:, input_data:) - new(storage).call(auth_strategy:, input_data:) - end - - def initialize(storage) - @storage = storage - end - - # rubocop:disable Metrics/AbcSize - def call(auth_strategy:, input_data:) - username = Util.origin_user_id(caller: self.class, storage: @storage, auth_strategy:) - .on_failure { return it } - .result - - permissions = parse_permission_mask(input_data.user_permissions) - - Authentication[auth_strategy].call(storage: @storage) do |http| - with_tagged_logger do - info "Getting the folder information" - folder_info = FileInfoQuery.call(storage: @storage, auth_strategy:, file_id: input_data.file_id) - .on_failure { return it } - .result - - info "Setting permissions #{permissions.inspect} on #{folder_info.location}" - - body = request_xml_body(permissions[:groups], permissions[:users]) - # This can raise KeyErrors, we probably should just default to empty Arrays. - response = http.request("PROPPATCH", - UrlBuilder.url(@storage.uri, - "remote.php/dav/files", - username, - CGI.unescape(folder_info.location)), - xml: body) - - handle_response(response) - end - end - end - - # rubocop:enable Metrics/AbcSize - - private - - def parse_permission_mask(user_permissions) - user_permissions.each_with_object({ groups: {}, users: {} }) do |entry, aggregate| - if entry.key?(:user_id) - aggregate[:users][entry[:user_id]] = - PERMISSIONS_MAP.values_at(*(PERMISSIONS_KEYS & entry[:permissions])).sum - else - aggregate[:groups][entry[:group_id]] = - PERMISSIONS_MAP.values_at(*(PERMISSIONS_KEYS & entry[:permissions])).sum - end - end - end - - # rubocop:disable Metrics/AbcSize - def handle_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - doc = Nokogiri::XML(response.body.to_s) - if doc.xpath(SUCCESS_XPATH).present? - info "Permissions set" - ServiceResult.success(result: :success) - else - Util.error(:permission_not_set, "nc:acl properly has not been set for #{path}", error_data) - end - in { status: 404 } - Util.error(:not_found, "Outbound request destination not found", error_data) - in { status: 401 } - Util.error(:unauthorized, "Outbound request not authorized", error_data) - else - Util.error(:error, "Outbound request failed", error_data) - end - end - - def request_xml_body(groups_permissions, users_permissions) - Nokogiri::XML::Builder.new do |xml| - xml["d"].propertyupdate( - "xmlns:d" => "DAV:", - "xmlns:nc" => "http://nextcloud.org/ns" - ) do - xml["d"].set do - xml["d"].prop do - xml["nc"].send(:"acl-list") do - groups_permissions.each do |group, group_permissions| - xml["nc"].acl do - xml["nc"].send(:"acl-mapping-type", "group") - xml["nc"].send(:"acl-mapping-id", group) - xml["nc"].send(:"acl-mask", "31") - xml["nc"].send(:"acl-permissions", group_permissions.to_s) - end - end - users_permissions.each do |user, user_permissions| - xml["nc"].acl do - xml["nc"].send(:"acl-mapping-type", "user") - xml["nc"].send(:"acl-mapping-id", user) - xml["nc"].send(:"acl-mask", "31") - xml["nc"].send(:"acl-permissions", user_permissions.to_s) - end - end - end - end - end - end - end.to_xml - end - - # rubocop:enable Metrics/AbcSize - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/upload_link_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/upload_link_query.rb deleted file mode 100644 index 3137d17683c..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/upload_link_query.rb +++ /dev/null @@ -1,104 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class UploadLinkQuery - include TaggedLogging - - using ServiceResultRefinements - - def self.call(storage:, auth_strategy:, upload_data:) - new(storage).call(auth_strategy:, upload_data:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, upload_data:) - with_tagged_logger do - Authentication[auth_strategy].call(storage: @storage) do |http| - response = http.post(base_uri, json: payload_from(upload_data:)) - - handle_response(response).map do |rsp| - UploadLink.new(URI("#{upload_base_uri}/#{rsp[:token]}"), :post) - end - end - end - end - - private - - def base_uri - UrlBuilder.url(@storage.uri, "index.php/apps/integration_openproject/direct-upload-token") - end - - def upload_base_uri - UrlBuilder.url(@storage.uri, "index.php/apps/integration_openproject/direct-upload") - end - - def invalid?(upload_data:) - upload_data.folder_id.blank? || upload_data.file_name.blank? - end - - def payload_from(upload_data:) - { folder_id: upload_data.folder_id } - end - - def handle_response(response) - case response - in { status: 200..299 } - info "Request successful" - ServiceResult.success(result: response.json(symbolize_keys: true)) - in { status: 404 } - info "The parent folder was not found." - Util.failure(code: :not_found, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request destination not found!") - in { status: 401 } - info "User authorization failed." - Util.failure(code: :unauthorized, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request not authorized!") - else - info "Unknown error happened." - Util.failure(code: :error, - data: Util.error_data_from_response(caller: self.class, response:), - log_message: "Outbound request failed with unknown error!") - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/user_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/user_query.rb deleted file mode 100644 index edfb52df00c..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/user_query.rb +++ /dev/null @@ -1,82 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - class UserQuery - using ServiceResultRefinements - - def self.call(storage:, auth_strategy:) - new(storage).call(auth_strategy:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:) - Authentication[auth_strategy].call(storage: @storage, http_options: Util.ocs_api_request) do |http| - handle_response(http.get(UrlBuilder.url(@storage.uri, "/ocs/v1.php/cloud/user"))) - end - end - - private - - def handle_response(response) - case response - in { status: 200..299 } - handle_success_response(response) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, errors: StorageError.new(code: :unauthorized)) - else - data = StorageErrorData.new(source: self.class, payload: response) - ServiceResult.failure(result: :error, errors: StorageError.new(code: :error, data:)) - end - end - - def handle_success_response(response) - error_data = StorageErrorData.new(source: self.class, payload: response) - xml = Nokogiri::XML(response.body.to_s) - statuscode = xml.xpath("/ocs/meta/statuscode").text - - case statuscode - when "100" - ServiceResult.success(result: { id: xml.xpath("/ocs/data/id").text }) - else - Util.error(:error, "Unknown response body", error_data) - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/util.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/util.rb deleted file mode 100644 index af207745372..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/util.rb +++ /dev/null @@ -1,141 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module Nextcloud - module Util - using ServiceResultRefinements - - class << self - def ocs_api_request - { headers: { "OCS-APIRequest" => "true" } } - end - - def accept_json - { headers: { "Accept" => "application/json" } } - end - - def webdav_request_with_depth(number) - { headers: { "Depth" => number } } - end - - def storage_error(response:, code:, source:, log_message: nil) - # Some errors, like timeouts, aren't json responses so we need to adapt - data = StorageErrorData.new(source:, payload: response.to_s) - - StorageError.new(code:, data:, log_message:) - end - - def error(code, log_message = nil, data = nil) - ServiceResult.failure( - result: code, # This is needed to work with the ConnectionManager token refresh mechanism. - errors: StorageError.new(code:, log_message:, data:) - ) - end - - def error_text_from_response(response) - response.xml.xpath("//s:message").text - end - - # rubocop:disable Metrics/AbcSize - def origin_user_id(caller:, storage:, auth_strategy:) - case auth_strategy.key - when :basic_auth - ServiceResult.success(result: storage.username) - when :oauth_user_token, :sso_user_token - auth_source = if auth_strategy.key == :oauth_user_token - storage.oauth_client - else - auth_strategy.user.authentication_provider - end - origin_user_id = RemoteIdentity.where(user_id: auth_strategy.user, - auth_source:, - integration: storage) - .pick(:origin_user_id) - if origin_user_id.present? - ServiceResult.success(result: origin_user_id) - else - failure(code: :error, - data: StorageErrorData.new(source: caller), - log_message: - "No origin user ID or user token found. Cannot execute query without user context.") - end - else - failure(code: :error, - data: StorageErrorData.new(source: caller), - log_message: "No authentication strategy with user context found. " \ - "Cannot execute query without user context.") - end - end - # rubocop:enable Metrics/AbcSize - - def error_data_from_response(caller:, response:) - payload = if response.respond_to?(:content_type) - case response.content_type.mime_type - when "application/json" - response.json - when "text/xml", "application/xml" - response.xml - else - response.body.to_s - end - else - response.to_s - end - - StorageErrorData.new(source: caller, payload:) - end - - def failure(code:, data:, log_message:) - ServiceResult.failure(result: code, errors: StorageError.new(code:, data:, log_message:)) - end - - def storage_file_from_file_info(storage_file_info) - StorageFile.new( - id: storage_file_info.id, - name: storage_file_info.name, - size: storage_file_info.size, - mime_type: storage_file_info.mime_type, - created_at: storage_file_info.created_at, - last_modified_at: storage_file_info.last_modified_at, - created_by_name: storage_file_info.owner_name, - last_modified_by_name: storage_file_info.last_modified_by_name, - location: storage_file_info.location, - permissions: storage_file_info.permissions - ) - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/copy_template_folder_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/copy_template_folder_command.rb deleted file mode 100644 index 9d3355abfc8..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/copy_template_folder_command.rb +++ /dev/null @@ -1,105 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class CopyTemplateFolderCommand - include TaggedLogging - - def self.call(auth_strategy:, storage:, source_path:, destination_path:) - if source_path.blank? || destination_path.blank? - return ServiceResult.failure( - result: :error, - errors: StorageError.new(code: :error, - log_message: "Both source and destination paths need to be present") - ) - end - - new(storage).call(auth_strategy:, source_location: source_path, destination_name: destination_path) - end - - def initialize(storage) - @storage = storage - @data = ResultData::CopyTemplateFolder.new(id: nil, polling_url: nil, requires_polling: true) - end - - def call(auth_strategy:, source_location:, destination_name:) - with_tagged_logger do - info "Requesting Copy of folder #{source_location} to #{destination_name}" - Authentication[auth_strategy].call(storage: @storage) do |httpx| - handle_response( - httpx.post(url_for(source_location) + query, json: { name: destination_name }) - ) - end - end - end - - private - - def handle_response(response) - source = self.class - - case response - in { status: 202 } - ServiceResult.success(result: @data.with(polling_url: response.headers[:location])) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source:)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source:)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source:, - log_message: "Template folder not found")) - in { status: 409 } - ServiceResult.failure(result: :conflict, - errors: Util.storage_error( - response:, code: :conflict, source:, - log_message: "The copy would overwrite an already existing folder" - )) - else - ServiceResult.failure(result: :error, - errors: Util.storage_error(response:, code: :error, source:)) - end - end - - def url_for(source_location) - UrlBuilder.url(Util.drive_base_uri(@storage), "/items", source_location, "/copy") - end - - def query = "?@microsoft.graph.conflictBehavior=fail" - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/create_folder_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/create_folder_command.rb deleted file mode 100644 index 0d2b01ba90e..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/create_folder_command.rb +++ /dev/null @@ -1,104 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class CreateFolderCommand - include TaggedLogging - using ServiceResultRefinements - - 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:) - with_tagged_logger do - info "Creating folder #{folder_name} under #{parent_location} using #{auth_strategy.key}" - Authentication[auth_strategy].call(storage: @storage, http_options:) do |http| - handle_response http.post(url_for(parent_location), body: payload(folder_name)) - end - end - end - - private - - def http_options - Util.json_content_type - end - - def url_for(parent_location) - if parent_location.root? - UrlBuilder.url(Util.drive_base_uri(@storage), "/root/children") - else - UrlBuilder.url(Util.drive_base_uri(@storage), "/items", parent_location.path, "/children") - end - end - - def handle_response(response) - source = self.class - - case response - in { status: 200..299 } - info "Folder successfully created." - ServiceResult.success(result: - Util.storage_file_from_json(MultiJson.load(response.body, symbolize_keys: true))) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(code: :not_found, response:, source:)) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(code: :unauthorized, response:, source:)) - in { status: 409 } - ServiceResult.failure(result: :already_exists, - errors: Util.storage_error(code: :conflict, response:, source:)) - else - ServiceResult.failure(result: :error, - errors: Util.storage_error(code: :error, response:, source:)) - end - end - - def payload(folder_name) - { - name: folder_name, - folder: {}, - "@microsoft.graph.conflictBehavior" => "fail" - }.to_json - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/delete_folder_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/delete_folder_command.rb deleted file mode 100644 index f2ca8f88f6e..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/delete_folder_command.rb +++ /dev/null @@ -1,79 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class DeleteFolderCommand - def self.call(storage:, auth_strategy:, location:) - new(storage).call(auth_strategy:, location:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, location:) - Authentication[auth_strategy].call(storage: @storage) do |http| - handle_response http.delete( - UrlBuilder.url(Util.drive_base_uri(@storage), "items", location) - ) - end - end - - private - - def handle_response(response) - data = ::Storages::StorageErrorData.new(source: self.class, payload: response) - - case response - in { status: 200..299 } - # The service returns a 204 with an empty body - ServiceResult.success - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: ::Storages::StorageError.new(code: :unauthorized, data:)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: ::Storages::StorageError.new(code: :not_found, data:)) - in { status: 409 } - ServiceResult.failure(result: :conflict, - errors: ::Storages::StorageError.new(code: :conflict, data:)) - else - ServiceResult.failure(result: :error, - errors: ::Storages::StorageError.new(code: :error, data:)) - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/download_link_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/download_link_query.rb deleted file mode 100644 index b18f11532a9..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/download_link_query.rb +++ /dev/null @@ -1,88 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class DownloadLinkQuery - def self.call(storage:, auth_strategy:, file_link:) - new(storage).call(auth_strategy:, file_link:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, file_link:) - if file_link.nil? - return ServiceResult.failure(result: :error, - errors: Util.storage_error(code: :error, response: nil, source: self.class, - log_message: "File link can not be nil.")) - end - - Authentication[auth_strategy].call(storage: @storage) do |http| - handle_errors http.get(url_for(file_link.origin_id)) - end - end - - private - - def handle_errors(response) - case response - in { status: 300..399 } - ServiceResult.success(result: response.headers["Location"]) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(code: :not_found, response:, source: self.class, - log_message: "Outbound request destination not found!")) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(code: :forbidden, response:, source: self.class, - log_message: "Outbound request forbidden!")) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(code: :unauthorized, response:, source: self.class, - log_message: "Outbound request not authorized!")) - else - ServiceResult.failure(result: :error, - errors: Util.storage_error(code: :error, response:, source: self.class, - log_message: "Outbound request failed with unknown error!")) - end - end - - def url_for(file_id) - UrlBuilder.url(Util.drive_base_uri(@storage), "items", file_id, "content") - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/file_info_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/file_info_query.rb deleted file mode 100644 index 7aa72df73f6..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/file_info_query.rb +++ /dev/null @@ -1,114 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class FileInfoQuery - FIELDS = %w[id name fileSystemInfo file folder size createdBy lastModifiedBy parentReference].freeze - - def self.call(storage:, auth_strategy:, file_id:) - new(storage).call(auth_strategy:, file_id:) - end - - def initialize(storage) - @storage = storage - @drive_item_query = Internal::DriveItemQuery.new(storage) - @error_data = StorageErrorData.new(source: self.class) - end - - def call(auth_strategy:, file_id:) - validation = validate_input(file_id) - return validation if validation.failure? - - requested_result = Authentication[auth_strategy].call(storage: @storage) do |http| - @drive_item_query.call(http:, drive_item_id: file_id, fields: FIELDS) - end - - requested_result.on_success { |sr| return ServiceResult.success(result: storage_file_info(sr.result)) } - requested_result.on_failure do |sr| - return sr unless sr.result == :not_found && auth_strategy.user.present? - - return admin_query(file_id) - end - end - - private - - def admin_query(file_id) - admin_result = Authentication[userless_strategy].call(storage: @storage) do |http| - @drive_item_query.call(http:, drive_item_id: file_id, fields: FIELDS) - end - - admin_result.on_success do |admin_query| - return ServiceResult.success( - result: storage_file_info(admin_query.result, status: "forbidden", status_code: 403) - ) - end - end - - def validate_input(file_id) - if file_id.nil? - ServiceResult.failure( - result: :error, - errors: StorageError.new(code: :error, - data: @error_data, log_message: "File ID can not be nil") - ) - else - ServiceResult.success - end - end - - def userless_strategy = Registry.resolve("one_drive.authentication.userless").call - - def storage_file_info(json, status: "ok", status_code: 200) # rubocop:disable Metrics/AbcSize - StorageFileInfo.new( - status:, - status_code:, - id: json[:id], - name: json[:name], - mime_type: Util.mime_type(json), - size: json[:size], - owner_name: json.dig(:createdBy, :user, :displayName), - owner_id: json.dig(:createdBy, :user, :id), - permissions: nil, - location: UrlBuilder.path(Util.extract_location(json[:parentReference], json[:name])), - last_modified_at: Time.zone.parse(json.dig(:fileSystemInfo, :lastModifiedDateTime)), - created_at: Time.zone.parse(json.dig(:fileSystemInfo, :createdDateTime)), - last_modified_by_name: json.dig(:lastModifiedBy, :user, :displayName), - last_modified_by_id: json.dig(:lastModifiedBy, :user, :id) - ) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/file_path_to_id_map_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/file_path_to_id_map_query.rb deleted file mode 100644 index 89fdefe5115..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/file_path_to_id_map_query.rb +++ /dev/null @@ -1,123 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class FilePathToIdMapQuery - CHILDREN_FIELDS = %w[id name file folder parentReference].freeze - FOLDER_FIELDS = %w[id name parentReference].freeze - - def self.call(storage:, auth_strategy:, folder:, depth: Float::INFINITY) - new(storage).call(auth_strategy:, folder:, depth:) - end - - def initialize(storage) - @storage = storage - @children_query = Internal::ChildrenQuery.new(storage) - @drive_item_query = Internal::DriveItemQuery.new(storage) - end - - # rubocop:disable Metrics/AbcSize - def call(auth_strategy:, folder:, depth:) - Authentication[auth_strategy].call(storage: @storage) do |http| - fetched_folder = fetch_folder(http, folder) - .on_failure { return it } - .result - - file_ids_dictionary = fetched_folder - queue = [folder] - level = 0 - - while queue.any? && level < depth - dir = queue.shift - - visit = visit(http, dir) - return visit if visit.failure? - - entry, to_queue = visit.result.values_at(:entry, :to_queue) - file_ids_dictionary = file_ids_dictionary.merge(entry) - queue.concat(to_queue) - level += 1 - end - - ServiceResult.success(result: file_ids_dictionary) - end - end - - # rubocop:enable Metrics/AbcSize - - private - - def visit(http, folder) - call = @children_query.call(http:, folder:, fields: CHILDREN_FIELDS) - return call if call.failure? - - entry = {} - to_queue = [] - - call.result[:value].each do |json| - new_entry, folder = parse_drive_item_info(json).values_at(:entry, :folder) - - entry = entry.merge(new_entry) - if folder.present? - to_queue.append(folder) - end - end - - ServiceResult.success(result: { entry:, to_queue: }) - end - - def parse_drive_item_info(json) - drive_item_id = json[:id] - location = Util.extract_location(json[:parentReference], json[:name]) - - entry = { location => StorageFileId.new(id: drive_item_id) } - folder = json[:folder].present? ? ParentFolder.new(drive_item_id) : nil - - { entry:, folder: } - end - - def fetch_folder(http, folder) - result = @drive_item_query.call(http:, drive_item_id: folder.path, fields: FOLDER_FIELDS) - result.map do |json| - if folder.root? - { "/" => StorageFileId.new(id: json[:id]) } - else - parse_drive_item_info(json)[:entry] - end - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb deleted file mode 100644 index d1d7a998214..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_info_query.rb +++ /dev/null @@ -1,92 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class FilesInfoQuery - include TaggedLogging - using ServiceResultRefinements - - def self.call(storage:, auth_strategy:, file_ids: []) - new(storage).call(auth_strategy:, file_ids:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, file_ids:) - if file_ids.nil? - return ServiceResult.failure( - result: :error, - errors: StorageError.new(code: :error, log_message: "File IDs can not be nil") - ) - end - - with_tagged_logger do - info "Retrieving file information for #{file_ids.join(', ')}" - result = Array(file_ids).map do |file_id| - file_info_result = FileInfoQuery.call(storage: @storage, auth_strategy:, file_id:) - - file_info_result.on_failure do |failed_result| - return failed_result if failed_result.error_source.module_parent == AuthenticationStrategies - end - - wrap_storage_file_error(file_id, file_info_result) - end - - ServiceResult.success(result:) - end - end - - private - - def wrap_storage_file_error(file_id, query_result) - return query_result.result if query_result.success? - - status = if query_result.error_payload.instance_of?(HTTPX::ErrorResponse) - query_result.error_payload.error - else - query_result.error_payload.dig(:error, :code) - end - - StorageFileInfo.new( - id: file_id, - status:, - status_code: Rack::Utils::SYMBOL_TO_STATUS_CODE[query_result.errors.code] || 500 - ) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_query.rb deleted file mode 100644 index 74f88aca9ff..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/files_query.rb +++ /dev/null @@ -1,181 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class FilesQuery - include TaggedLogging - - FIELDS = "?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference" - - def self.call(storage:, auth_strategy:, folder:) - new(storage).call(auth_strategy:, folder:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, folder:) - with_tagged_logger do - info "Getting data on all files under folder '#{folder}' using #{auth_strategy.key}" - validate_input_data(folder).on_failure { return it } - - Authentication[auth_strategy].call(storage: @storage) do |http| - response = handle_response(http.get(children_url_for(folder) + FIELDS), :value) - - if response.result.empty? - empty_response(http, folder) - else - response.map { |json_files| storage_files(json_files) } - end - end - end - end - - private - - def validate_input_data(folder) - if folder.is_a?(ParentFolder) - ServiceResult.success - else - data = StorageErrorData.new(source: self.class) - log_message = "Folder input is not a ParentFolder object." - ServiceResult.failure(result: :error, errors: StorageError.new(code: :error, log_message:, data:)) - end - end - - # rubocop:disable Metrics/AbcSize - def handle_response(response, map_value) - case response - in { status: 200..299 } - ServiceResult.success(result: response.json(symbolize_keys: true).fetch(map_value)) - in { status: 400 } - ServiceResult.failure(result: :request_error, - errors: Util.storage_error(response:, code: :request_error, source: self.class)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source: self.class)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source: self.class)) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source: self.class)) - else - data = StorageErrorData.new(source: self.class, payload: response) - ServiceResult.failure(result: :error, errors: StorageError.new(code: :error, data:)) - end - end - - # rubocop:enable Metrics/AbcSize - - def storage_files(json_files) - files = json_files.map { |json| Util.storage_file_from_json(json) } - - parent_reference = json_files.first[:parentReference] - StorageFiles.new(files, parent(parent_reference), forge_ancestors(parent_reference)) - end - - def empty_response(http, folder) - handle_response(http.get(location_url_for(folder) + FIELDS), :id).map do |parent_location_id| - empty_storage_files(folder.path, parent_location_id) - end - end - - def empty_storage_files(path, parent_id) - StorageFiles.new( - [], - StorageFile.new( - id: parent_id, - name: path.split("/").last, - location: UrlBuilder.path(path), - permissions: %i[readable writeable] - ), - forge_ancestors(path:) - ) - end - - def parent(parent_reference) - _, _, name = parent_reference[:path].gsub(/.*root:/, "").rpartition "/" - - if name.empty? - root(parent_reference[:id]) - else - StorageFile.new( - id: parent_reference[:id], - name:, - location: UrlBuilder.path(Util.extract_location(parent_reference)), - permissions: %i[readable writeable] - ) - end - end - - def forge_ancestors(parent_reference) - path_elements = parent_reference[:path].gsub(/.+root:/, "").split("/") - - path_elements[0..-2].map do |component| - next root(Digest::SHA256.hexdigest("i_am_root")) if component.blank? - - StorageFile.new( - id: Digest::SHA256.hexdigest(component), - name: component, - location: UrlBuilder.path(component) - ) - end - end - - def root(id) - StorageFile.new(name: "Root", - location: "/", - id:, - permissions: %i[readable writeable]) - end - - def children_url_for(folder) - base_uri = Util.drive_base_uri(@storage) - return UrlBuilder.url(base_uri, "/root/children") if folder.root? - - "#{UrlBuilder.url(base_uri, '/root')}:#{UrlBuilder.path(folder.path)}:/children" - end - - def location_url_for(folder) - base_uri = UrlBuilder.url(Util.drive_base_uri(@storage), "/root") - return base_uri if folder.root? - - "#{base_uri}:#{UrlBuilder.path(folder.path)}" - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/children_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/children_query.rb deleted file mode 100644 index c60dd3ab95f..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/children_query.rb +++ /dev/null @@ -1,89 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - module Internal - class ChildrenQuery - Util = ::Storages::Peripherals::StorageInteraction::OneDrive::Util - - def initialize(storage) - @storage = storage - end - - def call(http:, folder:, fields: []) - query = if fields.empty? - "" - else - "?$select=#{fields.join(',')}" - end - - make_children_request(folder, http, query) - end - - private - - def make_children_request(folder, http, query) - url = UrlBuilder.url(Util.drive_base_uri(@storage), uri_path_for(folder)) - handle_responses(http.get(url + query)) - end - - def handle_responses(response) - case response - in { status: 200..299 } - ServiceResult.success(result: response.json(symbolize_keys: true)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source: self)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source: self)) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source: self)) - else - data = ::Storages::StorageErrorData.new(source: self.class, payload: response) - ServiceResult.failure(result: :error, errors: ::Storages::StorageError.new(code: :error, data:)) - end - end - - def uri_path_for(folder) - return "/root/children" if folder.root? - - "/items/#{folder.path}/children" - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb deleted file mode 100644 index 40efd13cece..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/internal/drive_item_query.rb +++ /dev/null @@ -1,89 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - module Internal - class DriveItemQuery - def initialize(storage) - @storage = storage - end - - def call(http:, drive_item_id:, fields: []) - select_url_query = if fields.empty? - "" - else - "?$select=#{fields.join(',')}" - end - - make_file_request(drive_item_id, http, select_url_query) - end - - private - - def make_file_request(drive_item_id, http, select_url_query) - url = UrlBuilder.url(Util.drive_base_uri(@storage), uri_path_for(drive_item_id)) - handle_response http.get("#{url}#{select_url_query}") - end - - def handle_response(response) - case response - in { status: 200..299 } - ServiceResult.success(result: response.json(symbolize_keys: true)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source: self.class)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source: self.class)) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source: self.class)) - else - data = StorageErrorData.new(source: self.class, payload: response) - ServiceResult.failure(result: :error, errors: StorageError.new(code: :error, data:)) - end - end - - def uri_path_for(file_id) - if file_id == "/" - "/root" - else - "/items/#{file_id}" - end - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_storage_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_storage_query.rb deleted file mode 100644 index a6c23db9cc5..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/open_storage_query.rb +++ /dev/null @@ -1,88 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class OpenStorageQuery - def self.call(storage:, auth_strategy:) - new(storage).call(auth_strategy:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:) - Authentication[auth_strategy].call(storage: @storage) do |http| - request_drive(http).map(&web_url) - end - end - - private - - def request_drive(http) - handle_responses http.get(request_url) - end - - def handle_responses(response) - case response - in { status: 200..299 } - ServiceResult.success(result: response.json(symbolize_keys: true)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source: self.class)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source: self.class)) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source: self.class)) - else - ServiceResult.failure(result: :error, - errors: Util.storage_error(response:, code: :error, source: self.class)) - end - end - - def request_url - "#{Util.drive_base_uri(@storage)}?$select=webUrl" - end - - def web_url - ->(json) do - json[:webUrl] - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/rename_file_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/rename_file_command.rb deleted file mode 100644 index 71466dac229..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/rename_file_command.rb +++ /dev/null @@ -1,94 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class RenameFileCommand - def self.call(storage:, auth_strategy:, file_id:, name:) - new(storage).call(auth_strategy:, file_id:, name:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, file_id:, name:) - validate_input_data(file_id:, name:).on_failure { |failure| return failure } - - Authentication[auth_strategy].call(storage: @storage, http_options: Util.json_content_type) do |http| - handle_response http.patch(UrlBuilder.url(Util.drive_base_uri(@storage), "items", file_id), - body: { name: }.to_json) - end - end - - private - - def validate_input_data(file_id:, name:) - if file_id.blank? || name.blank? - ServiceResult.failure(result: :error, - errors: StorageError.new(code: :error, - data: StorageErrorData.new(source: self.class), - log_message: "Invalid input data!")) - else - ServiceResult.success - end - end - - # rubocop:disable Metrics/AbcSize - def handle_response(response) - case response - in { status: 200..299 } - ServiceResult.success(result: Util.storage_file_from_json(response.json(symbolize_keys: true))) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source: self.class)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source: self.class)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source: self.class)) - in { status: 409 } - ServiceResult.failure(result: :conflict, - errors: Util.storage_error(response:, code: :conflict, source: self.class)) - else - ServiceResult.failure(result: :error, - errors: Util.storage_error(response:, code: :error, source: self.class)) - end - end - - # rubocop:enable Metrics/AbcSize - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/set_permissions_command.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/set_permissions_command.rb deleted file mode 100644 index 7e058742d6d..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/set_permissions_command.rb +++ /dev/null @@ -1,199 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class SetPermissionsCommand - include TaggedLogging - - using ServiceResultRefinements - - PermissionUpdateData = Data.define(:role, :permission_ids, :user_ids, :drive_item_id) do - def create? = permission_ids.empty? && user_ids.any? - - def delete? = permission_ids.any? && user_ids.empty? - - def update? = permission_ids.any? && user_ids.any? - end - - # Instantiates the command and executes it. - # - # @param storage [Storage] The storage to interact with. - # @param auth_strategy [AuthenticationStrategy] The authentication strategy to use. - # @param input_data [Inputs::SetPermissions] The data needed for setting permissions, containing the file id - # and the permissions for an array of users. - def self.call(storage:, auth_strategy:, input_data:) - new(storage).call(auth_strategy:, input_data:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, input_data:) - with_tagged_logger do - Authentication[auth_strategy].call(storage: @storage) do |http| - item = input_data.file_id - item_exists?(http, item).on_failure { return it } - - current_permissions = get_current_permissions(http, item).on_failure { return it }.result - info "Read and write permissions found: #{current_permissions}" - - role_to_user_map(input_data).each_pair do |role, user_ids| - apply_permission_changes( - PermissionUpdateData.new(role:, - user_ids:, - permission_ids: current_permissions[role], - drive_item_id: item), - http - ) - end - - ServiceResult.success - end - end - end - - private - - def role_to_user_map(input_data) - input_data.user_permissions - .each_with_object({ read: [], write: [] }) do |user_permission_set, map| - if user_permission_set[:permissions].include?(:write_files) - map[:write] << user_permission_set[:user_id] - elsif user_permission_set[:permissions].include?(:read_files) - map[:read] << user_permission_set[:user_id] - end - end - end - - def item_exists?(http, item_id) - info "Checking if folder #{item_id} exists" - handle_response(http.get(item_path(item_id))) - end - - def get_current_permissions(http, path) - info "Getting current permissions for #{path}" - handle_response(http.get(permissions_path(path))).map { |result| extract_permission_ids(result[:value]) } - end - - def apply_permission_changes(update_data, http) - return delete_permissions(update_data, http) if update_data.delete? - return create_permissions(update_data, http) if update_data.create? - - update_permissions(update_data, http) if update_data.update? - end - - def update_permissions(update_data, http) - info "Updating permissions on #{update_data.drive_item_id}" - delete_permissions(update_data, http) - create_permissions(update_data, http) - end - - def create_permissions(update_data, http) - drive_recipients = update_data.user_ids.map { |id| { objectId: id } } - - info "Creating #{update_data.role} permissions on #{update_data.drive_item_id} for #{drive_recipients}" - response = http.post(invite_path(update_data.drive_item_id), - json: { - requireSignIn: true, - sendInvitation: false, - roles: [update_data.role], - recipients: drive_recipients - }) - - handle_response(response).result_or { |error| log_storage_error(error) } - end - - def delete_permissions(update_data, http) - info "Removing permissions on #{update_data.drive_item_id}" - - update_data.permission_ids.each do |permission_id| - handle_response( - http.delete(permission_path(update_data.drive_item_id, permission_id)) - ).result_or { |error| log_storage_error(error) } - end - end - - FILTER_LAMBDA = lambda { |role, permission| - next unless permission[:roles].member?(role) - - permission[:id] - }.curry - - def extract_permission_ids(permission_set) - write_permissions = permission_set.filter_map(&FILTER_LAMBDA.call("write")) - read_permissions = permission_set.filter_map(&FILTER_LAMBDA.call("read")) - - { read: read_permissions, write: write_permissions } - end - - def handle_response(response) - source = self.class - - case response - in { status: 200 } - ServiceResult.success(result: response.json(symbolize_keys: true)) - in { status: 204 } - ServiceResult.success(result: response) - in { status: 400 } - ServiceResult.failure(result: :bad_request, - errors: Util.storage_error(response:, code: :bad_request, source:)) - in { status: 401 } - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(response:, code: :unauthorized, source:)) - in { status: 403 } - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(response:, code: :forbidden, source:)) - in { status: 404 } - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(response:, code: :not_found, source:)) - else - ServiceResult.failure(result: :error, - errors: Util.storage_error(response:, code: :error, source:)) - end - end - - def permission_path(item_id, permission_id) = "#{permissions_path(item_id)}/#{permission_id}" - - def permissions_path(item_id) = "#{item_path(item_id)}/permissions" - - def invite_path(item_id) = "#{item_path(item_id)}/invite" - - def item_path(item_id) - UrlBuilder.url(Util.drive_base_uri(@storage), "/items", item_id) - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/upload_link_query.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/upload_link_query.rb deleted file mode 100644 index db612317e9e..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/upload_link_query.rb +++ /dev/null @@ -1,104 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - class UploadLinkQuery - include TaggedLogging - - def self.call(storage:, auth_strategy:, upload_data:) - new(storage).call(auth_strategy:, upload_data:) - end - - def initialize(storage) - @storage = storage - end - - def call(auth_strategy:, upload_data:) - with_tagged_logger do - Authentication[auth_strategy].call(storage: @storage) do |http| - info "Requesting an upload link on folder #{upload_data.folder_id}" - handle_response http.post(url(upload_data.folder_id, upload_data.file_name), - json: payload(upload_data.file_name)) - end - end - end - - private - - def invalid?(upload_data:) - upload_data.folder_id.blank? || upload_data.file_name.blank? - end - - def payload(filename) - { item: { "@microsoft.graph.conflictBehavior" => "rename", name: filename } } - end - - # rubocop:disable Metrics/AbcSize - def handle_response(response) - case response - in { status: 200..299 } - upload_url = response.json(symbolize_keys: true)[:uploadUrl] - info "Upload link generated successfully." - ServiceResult.success(result: UploadLink.new(URI(upload_url), :put)) - in { status: 404 | 400 } # not existent parent folder in request url is responded with 400 - info "The parent folder was not found." - ServiceResult.failure(result: :not_found, - errors: Util.storage_error(code: :not_found, response:, source: self.class)) - in { status: 401 } - info "User authorization failed." - ServiceResult.failure(result: :unauthorized, - errors: Util.storage_error(code: :unauthorized, response:, source: self.class)) - in { status: 403 } - info "User authorization failed." - ServiceResult.failure(result: :forbidden, - errors: Util.storage_error(code: :forbidden, response:, source: self.class)) - else - info "Unknown error happened." - ServiceResult.failure(result: :error, - errors: Util.storage_error(code: :error, response:, source: self.class)) - end - end - - # rubocop:enable Metrics/AbcSize - - def url(folder, filename) - base = UrlBuilder.url(Util.drive_base_uri(@storage), "/items/", folder) - file_path = UrlBuilder.path(filename) - - "#{base}:#{file_path}:/createUploadSession" - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb deleted file mode 100644 index f16cfffe4a3..00000000000 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/one_drive/util.rb +++ /dev/null @@ -1,85 +0,0 @@ -# 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 Peripherals - module StorageInteraction - module OneDrive - module Util - using ServiceResultRefinements - - class << self - def mime_type(json) - json.dig(:file, :mimeType) || (json.key?(:folder) ? "application/x-op-directory" : nil) - end - - def storage_error(response:, code:, source:, log_message: nil) - # Some errors, like timeouts, aren't json responses so we need to adapt - payload = response.respond_to?(:json) ? response.json(symbolize_keys: true) : response.to_s - data = StorageErrorData.new(source:, payload:) - - StorageError.new(code:, data:, log_message:) - end - - def drive_base_uri(storage) - URI.parse(UrlBuilder.url(storage.uri, "/v1.0/drives", storage.drive_id)) - end - - def json_content_type - { headers: { "Content-Type" => "application/json" } } - end - - def extract_location(parent_reference, file_name = "") - location = parent_reference[:path].gsub(/.*root:/, "") - - appendix = file_name.blank? ? "" : "/#{file_name}" - location.empty? ? "/#{file_name}" : "#{location}#{appendix}" - end - - def storage_file_from_json(json) - StorageFile.new( - id: json[:id], - name: json[:name], - size: json[:size], - mime_type: Util.mime_type(json), - created_at: Time.zone.parse(json.dig(:fileSystemInfo, :createdDateTime)), - last_modified_at: Time.zone.parse(json.dig(:fileSystemInfo, :lastModifiedDateTime)), - created_by_name: json.dig(:createdBy, :user, :displayName), - last_modified_by_name: json.dig(:lastModifiedBy, :user, :displayName), - location: UrlBuilder.path(Util.extract_location(json[:parentReference], json[:name])), - permissions: %i[readable writeable] - ) - end - end - end - end - end - end -end diff --git a/modules/storages/app/common/storages/tagged_logging.rb b/modules/storages/app/common/storages/tagged_logging.rb index 2a5e353435e..c9f5e742264 100644 --- a/modules/storages/app/common/storages/tagged_logging.rb +++ b/modules/storages/app/common/storages/tagged_logging.rb @@ -38,6 +38,22 @@ module Storages logger.tagged(*tag, &) end + # @param error [Storages::Adapters::Results::Error] an instance of Storages::Adapters::Results::Error + # @param context [Hash{Symbol => Object}] extra metadata that will be appended to the logs + def log_adapter_error(error, context = {}) + payload = error.payload + data = + case payload + in { status: Integer } + { status: payload&.status, body: payload&.body.to_s } + else + payload.to_s + end + + error_message = context.merge({ error_code: error.code, data: }) + error error_message + end + # @param storage_error [Storages::StorageError] an instance of Storages::StorageError # @param context [Hash{Symbol => Object}] extra metadata that will be appended to the logs def log_storage_error(storage_error, context = {}) @@ -54,10 +70,10 @@ module Storages error error_message end + # @param validation_result [Dry::Validation::Result] + # @param context [Hash{Symbol, Object}] def log_validation_error(validation_result, context = {}) - # rubocop:disable Rails/DeprecatedActiveModelErrorsMethods error context.merge({ validation_message: validation_result.errors.to_h }) - # rubocop:enable Rails/DeprecatedActiveModelErrorsMethods end def logger diff --git a/modules/storages/app/components/storages/admin/side_panel/health_status_component.rb b/modules/storages/app/components/storages/admin/side_panel/health_status_component.rb index c104f859a60..7346d67360e 100644 --- a/modules/storages/app/components/storages/admin/side_panel/health_status_component.rb +++ b/modules/storages/app/components/storages/admin/side_panel/health_status_component.rb @@ -39,7 +39,7 @@ module Storages private def report - validator = Peripherals::Registry.resolve("#{model}.validators.connection").new(model) + validator = Adapters::Registry.resolve("#{model}.validators.connection").new(model) Rails.cache.read(validator.report_cache_key) end end diff --git a/modules/storages/app/components/storages/admin/storage_view_component.html.erb b/modules/storages/app/components/storages/admin/storage_view_component.html.erb index 269abc97890..391baedeedd 100644 --- a/modules/storages/app/components/storages/admin/storage_view_component.html.erb +++ b/modules/storages/app/components/storages/admin/storage_view_component.html.erb @@ -17,9 +17,9 @@ component.with_row(scheme: :default) do if wizard_step == step_name - render(Storages::Peripherals::Registry.resolve("#{storage}.components.forms.#{step_name}").new(storage, in_wizard: true)) + render(Storages::Adapters::Registry.resolve("#{storage}.components.forms.#{step_name}").new(storage, in_wizard: true)) else - render(Storages::Peripherals::Registry.resolve("#{storage}.components.#{step_name}").new(storage)) + render(Storages::Adapters::Registry.resolve("#{storage}.components.#{step_name}").new(storage)) end end end diff --git a/modules/storages/app/components/storages/project_storages/members/row_component.rb b/modules/storages/app/components/storages/project_storages/members/row_component.rb index 81c3ae76d7a..bc871c6fa49 100644 --- a/modules/storages/app/components/storages/project_storages/members/row_component.rb +++ b/modules/storages/app/components/storages/project_storages/members/row_component.rb @@ -99,13 +99,11 @@ module Storages::ProjectStorages::Members def storage_connection_status if storage_connected? return :connected if can_read_files? - return :connected_no_permissions end - selector = Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector.new(user: member.principal, storage:) - return :not_connected_sso if selector.sso? - return :not_connected_oauth2 if selector.storage_oauth? + return :not_connected_sso if storage.authenticate_via_idp? && member.principal.provided_by_oidc? + return :not_connected_oauth2 if storage.authenticate_via_storage? :not_connectable end diff --git a/modules/storages/app/contracts/storages/storages/base_contract.rb b/modules/storages/app/contracts/storages/storages/base_contract.rb index 6d1c68ed921..62faef2c84d 100644 --- a/modules/storages/app/contracts/storages/storages/base_contract.rb +++ b/modules/storages/app/contracts/storages/storages/base_contract.rb @@ -90,7 +90,7 @@ module Storages::Storages end def default_provider_contract - ::Storages::Peripherals::Registry.resolve("#{model.short_provider_type}.contracts.storage") + ::Storages::Adapters::Registry.resolve("#{model.short_provider_type}.contracts.storage") end end end diff --git a/modules/storages/app/contracts/storages/storages/nextcloud_audience_contract.rb b/modules/storages/app/contracts/storages/storages/nextcloud_audience_contract.rb deleted file mode 100644 index 826989a7c18..00000000000 --- a/modules/storages/app/contracts/storages/storages/nextcloud_audience_contract.rb +++ /dev/null @@ -1,49 +0,0 @@ -# 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 :storage_audience - validates :storage_audience, presence: true, if: -> { nextcloud_storage_authenticate_via_idp? } - - # Adding this to allow writing the storage_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 diff --git a/modules/storages/app/contracts/storages/storages/nextcloud_automatic_management_contract.rb b/modules/storages/app/contracts/storages/storages/nextcloud_automatic_management_contract.rb deleted file mode 100644 index 99636bcf224..00000000000 --- a/modules/storages/app/contracts/storages/storages/nextcloud_automatic_management_contract.rb +++ /dev/null @@ -1,69 +0,0 @@ -# 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 diff --git a/modules/storages/app/contracts/storages/storages/nextcloud_general_information_contract.rb b/modules/storages/app/contracts/storages/storages/nextcloud_general_information_contract.rb deleted file mode 100644 index 3903440a4b0..00000000000 --- a/modules/storages/app/contracts/storages/storages/nextcloud_general_information_contract.rb +++ /dev/null @@ -1,55 +0,0 @@ -# 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 } - - validate :require_ee_token_for_sso - - def require_ee_token_for_sso - return if EnterpriseToken.allows_to?(:nextcloud_sso) - return unless model.authenticate_via_idp? - return unless model.authentication_method_changed? - - plan_name = I18n.t("ee.upsell.plan_name", plan: OpenProject::Token.lowest_plan_for(:nextcloud_sso)&.capitalize) - errors.add(:authentication_method, :enterprise_plan_required, plan_name:) - end - end -end diff --git a/modules/storages/app/contracts/storages/storages/one_drive_contract.rb b/modules/storages/app/contracts/storages/storages/one_drive_contract.rb deleted file mode 100644 index d9c79fbe46a..00000000000 --- a/modules/storages/app/contracts/storages/storages/one_drive_contract.rb +++ /dev/null @@ -1,44 +0,0 @@ -# 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 OneDriveContract < ::ModelContract - attribute :name - validates :name, presence: true, length: { maximum: 255 } - attribute :host - validates :host, absence: true - attribute :tenant_id - validates :tenant_id, format: { with: /\A(?:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|consumers)\z/i } - attribute :drive_id - # GRAPH API considers drive ids of 16 characters or shorter as personal drive ids. Those are not supported, - # and allowing them lead to unexpected behavior. - validates :drive_id, presence: true, allow_nil: true, length: { minimum: 17 } - end -end diff --git a/modules/storages/app/controllers/storages/admin/automatically_managed_project_folders_controller.rb b/modules/storages/app/controllers/storages/admin/automatically_managed_project_folders_controller.rb index 2044430a9e8..6bd52fb31ee 100644 --- a/modules/storages/app/controllers/storages/admin/automatically_managed_project_folders_controller.rb +++ b/modules/storages/app/controllers/storages/admin/automatically_managed_project_folders_controller.rb @@ -99,7 +99,7 @@ class Storages::Admin::AutomaticallyManagedProjectFoldersController < Applicatio if service_result.success? redirect_to edit_admin_settings_storage_path(@storage) else - @wizard = ::Storages::Peripherals::Registry.resolve("#{@storage}.components.setup_wizard") + @wizard = ::Storages::Adapters::Registry.resolve("#{@storage}.components.setup_wizard") .new(model: @storage, user: current_user) render :edit end @@ -112,7 +112,7 @@ class Storages::Admin::AutomaticallyManagedProjectFoldersController < Applicatio component: Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent.new(@storage) ) - @wizard = ::Storages::Peripherals::Registry.resolve("#{@storage}.components.setup_wizard") + @wizard = ::Storages::Adapters::Registry.resolve("#{@storage}.components.setup_wizard") .new(model: @storage, user: current_user) respond_with_turbo_streams do |format| format.html { render :edit } diff --git a/modules/storages/app/controllers/storages/admin/health_status_controller.rb b/modules/storages/app/controllers/storages/admin/health_status_controller.rb index 2656416632f..40fd6984346 100644 --- a/modules/storages/app/controllers/storages/admin/health_status_controller.rb +++ b/modules/storages/app/controllers/storages/admin/health_status_controller.rb @@ -95,7 +95,7 @@ module Storages end def validator - @validator ||= Peripherals::Registry.resolve("#{@storage}.validators.connection").new(@storage) + @validator ||= Adapters::Registry.resolve("#{@storage}.validators.connection").new(@storage) end end end diff --git a/modules/storages/app/controllers/storages/admin/project_storages_controller.rb b/modules/storages/app/controllers/storages/admin/project_storages_controller.rb index cb3fc00b45c..3cc08c667c2 100644 --- a/modules/storages/app/controllers/storages/admin/project_storages_controller.rb +++ b/modules/storages/app/controllers/storages/admin/project_storages_controller.rb @@ -77,8 +77,7 @@ class Storages::Admin::ProjectStoragesController < Projects::SettingsController def oauth_access_grant @project_storage = @object storage = @project_storage.storage - auth_state = ::Storages::Peripherals::StorageInteraction::Authentication - .authorization_state(storage:, user: current_user) + auth_state = ::Storages::Adapters::Authentication.authorization_state(storage:, user: current_user) if auth_state == :connected redirect_to(external_file_storages_project_settings_project_storages_path) @@ -150,10 +149,7 @@ class Storages::Admin::ProjectStoragesController < Projects::SettingsController end def available_storages - Storages::Storage - .visible - .not_enabled_for_project(@project) - .select(&:configured?) + Storages::Storage.visible.not_enabled_for_project(@project).select(&:configured?) end def redirect_to_project_storages_path_with_oauth_access_grant_confirmation(storage) diff --git a/modules/storages/app/controllers/storages/admin/storages_controller.rb b/modules/storages/app/controllers/storages/admin/storages_controller.rb index b74ef5e6253..2888bceb0c4 100644 --- a/modules/storages/app/controllers/storages/admin/storages_controller.rb +++ b/modules/storages/app/controllers/storages/admin/storages_controller.rb @@ -147,7 +147,7 @@ class Storages::Admin::StoragesController < ApplicationController else origin_component = params[:origin_component].presence || "general_information" update_via_turbo_stream( - component: ::Storages::Peripherals::Registry.resolve("#{@storage}.components.forms.#{origin_component}").new( + component: ::Storages::Adapters::Registry.resolve("#{@storage}.components.forms.#{origin_component}").new( @storage, in_wizard: params[:continue_wizard].present? ) @@ -276,7 +276,7 @@ class Storages::Admin::StoragesController < ApplicationController end def storage_wizard(storage) - ::Storages::Peripherals::Registry.resolve("#{storage}.components.setup_wizard") + ::Storages::Adapters::Registry.resolve("#{storage}.components.setup_wizard") .new(model: storage, user: current_user) end @@ -288,6 +288,6 @@ class Storages::Admin::StoragesController < ApplicationController 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}") + ::Storages::Adapters::Registry.resolve("#{storage_name}.contracts.#{origin_component}") end end diff --git a/modules/storages/app/controllers/storages/project_storages_controller.rb b/modules/storages/app/controllers/storages/project_storages_controller.rb index b00bfd9903d..46eb4cc3a64 100644 --- a/modules/storages/app/controllers/storages/project_storages_controller.rb +++ b/modules/storages/app/controllers/storages/project_storages_controller.rb @@ -47,21 +47,20 @@ class Storages::ProjectStoragesController < ApplicationController def open @project_storage.open(current_user).match( on_success: ->(url) { redirect_to url, allow_other_host: true }, - on_failure: ->(error) { show_error(error.to_s) } + on_failure: ->(error) { show_error(error.code.to_s) } ) end private def ensure_remote_identity - selector = Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector.new(user: current_user, storage:) - return unless selector.storage_oauth? - - case Storages::Peripherals::StorageInteraction::Authentication.authorization_state(storage:, user: current_user) - when :not_connected, :failed_authorization + case Storages::Adapters::Authentication.authorization_state(storage:, user: current_user) + when :not_connected redirect_to ensure_connection_url - when :error + when :error, :failed_authorization show_error(I18n.t("project_storages.open.remote_identity_error")) + else + true end end @@ -72,6 +71,7 @@ class Storages::ProjectStoragesController < ApplicationController folder_create_service.call(storage:, project_storages_scope: project_storage_scope).on_failure do |result| return show_error(result.errors.full_messages) end + @project_storage.reload end @@ -79,7 +79,7 @@ class Storages::ProjectStoragesController < ApplicationController return unless @project_storage.project_folder_automatic? result = test_folder_access - return if result.success? || result.errors.code != :forbidden + return if result.success? || result.failure.code != :forbidden # Note: The time this operation takes may still scale with the number of users. # If this becomes a problem, we will have to update downstream code to allow changing permissions for a few users instead of @@ -98,19 +98,19 @@ class Storages::ProjectStoragesController < ApplicationController end def folder_create_service - Storages::Peripherals::Registry.resolve("#{storage}.services.folder_create") + Storages::Adapters::Registry.resolve("#{storage}.services.upkeep_managed_folders") end def folder_permissions_service - Storages::Peripherals::Registry.resolve("#{storage}.services.folder_permissions") + Storages::Adapters::Registry.resolve("#{storage}.services.upkeep_managed_folder_permissions") end def file_info - Storages::Peripherals::Registry.resolve("#{storage}.queries.file_info") + Storages::Adapters::Registry.resolve("#{storage}.queries.file_info") end def user_bound - Storages::Peripherals::Registry.resolve("#{storage}.authentication.user_bound") + Storages::Adapters::Registry.resolve("#{storage}.authentication.user_bound") end def storage @@ -122,11 +122,9 @@ class Storages::ProjectStoragesController < ApplicationController end def test_folder_access - file_info.call( - storage:, - auth_strategy: user_bound.call(storage:, user: current_user), - file_id: @project_storage.project_folder_id - ) + Storages::Adapters::Input::FileInfo.build(file_id: @project_storage.project_folder_id).bind do |input_data| + file_info.call(storage:, auth_strategy: user_bound.call(current_user, storage), input_data:) + end end def show_error(message) diff --git a/modules/storages/app/models/storages/nextcloud_storage.rb b/modules/storages/app/models/storages/nextcloud_storage.rb index dd90a93ec7b..fee17bbfc21 100644 --- a/modules/storages/app/models/storages/nextcloud_storage.rb +++ b/modules/storages/app/models/storages/nextcloud_storage.rb @@ -52,7 +52,7 @@ module Storages store_attribute :provider_fields, :token_exchange_scope, :string def oauth_configuration - Peripherals::OAuthConfigurations::NextcloudConfiguration.new(self) + Adapters::Providers::Nextcloud::OAuthConfiguration.new(self) end def automatic_management_new_record? diff --git a/modules/storages/app/models/storages/one_drive_storage.rb b/modules/storages/app/models/storages/one_drive_storage.rb index 5beade3ee8e..c9f4306a91b 100644 --- a/modules/storages/app/models/storages/one_drive_storage.rb +++ b/modules/storages/app/models/storages/one_drive_storage.rb @@ -79,7 +79,7 @@ module Storages end def oauth_configuration - Peripherals::OAuthConfigurations::OneDriveConfiguration.new(self) + Adapters::Providers::OneDrive::OAuthConfiguration.new(self) end def uri diff --git a/modules/storages/app/models/storages/project_storage.rb b/modules/storages/app/models/storages/project_storage.rb index 9b4544f6d89..7f5d9293adf 100644 --- a/modules/storages/app/models/storages/project_storage.rb +++ b/modules/storages/app/models/storages/project_storage.rb @@ -83,17 +83,17 @@ module Storages end def open(user) - auth_strategy = Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user:, storage:) + auth_strategy = Adapters::Registry.resolve("#{storage}.authentication.user_bound").call(user, storage) - if project_folder_not_accessible?(user) - Peripherals::Registry - .resolve("#{storage}.queries.open_storage") - .call(storage:, auth_strategy:) - else - Peripherals::Registry - .resolve("#{storage}.queries.open_file_link") - .call(storage:, auth_strategy:, file_id: project_folder_id) - end + result = if project_folder_not_accessible?(user) + Adapters::Registry.resolve("#{storage}.queries.open_storage").call(storage:, auth_strategy:, input_data: nil) + else + open_file_link(auth_strategy) + end + + # FIXME: We probably could make this into a service by itself. + # so errors can be more descriptive. 2025-05-05 @mereghost + result.either(-> { ServiceResult.success(result: it) }, -> { ServiceResult.failure(errors: it) }) end def open_project_storage_url @@ -102,9 +102,14 @@ module Storages private + def open_file_link(auth_strategy) + Adapters::Input::OpenFileLink.build(file_id: project_folder_id).bind do |input_data| + Adapters::Registry.resolve("#{storage}.queries.open_file_link").call(storage:, auth_strategy:, input_data:) + end + end + def managed_folder_identifier - @managed_folder_identifier ||= - Peripherals::Registry.resolve("#{storage}.models.managed_folder_identifier").new(self) + @managed_folder_identifier ||= Adapters::Registry.resolve("#{storage}.models.managed_folder_identifier").new(self) end def project_folder_not_accessible?(user) diff --git a/modules/storages/app/models/storages/storage.rb b/modules/storages/app/models/storages/storage.rb index a270cf134a9..be4d9da44c1 100644 --- a/modules/storages/app/models/storages/storage.rb +++ b/modules/storages/app/models/storages/storage.rb @@ -120,16 +120,8 @@ module Storages end def oauth_access_granted?(user) - selector = Peripherals::StorageInteraction::AuthenticationMethodSelector.new( - storage: self, - user: - ) - case selector.authentication_method - when :sso - true - when :storage_oauth + (user.authentication_provider.is_a?(OpenIDConnect::Provider) && authenticate_via_idp?) || OAuthClientToken.exists?(user:, oauth_client:) - end end def health_notifications_should_be_sent? @@ -137,16 +129,6 @@ module Storages (health_notifications_enabled.nil? && automatic_management_enabled?) || health_notifications_enabled? end - def automatically_managed? - ActiveSupport::Deprecation.new.warn( - "`#automatically_managed?` is deprecated. Use `#automatic_management_enabled?` instead. " \ - "NOTE: The new method name better reflects the actual behavior of the storage. " \ - "It's not the storage that is automatically managed, rather the Project (Storage) Folder is. " \ - "A storage only has this feature enabled or disabled." - ) - super - end - def automatic_management_enabled? !!automatically_managed end @@ -240,13 +222,8 @@ module Storages end def extract_origin_user_id(token) - auth_strategy = ::Storages::Peripherals::Registry - .resolve("#{self}.authentication.specific_bearer_token") - .with_token(token.access_token) - ::Storages::Peripherals::Registry - .resolve("#{self}.queries.user") - .call(auth_strategy:, storage: self) - .map { |user| user[:id] } # rubocop:disable Rails/Pluck + auth_strategy = Adapters::Input::Strategy.build(key: :bearer_token, token: token.access_token) + Adapters::Registry.resolve("#{self}.queries.user").call(auth_strategy:, storage: self).fmap { it[:id] } end end end diff --git a/modules/storages/app/services/storages/base_service.rb b/modules/storages/app/services/storages/base_service.rb index a5dea3f631f..56fae373893 100644 --- a/modules/storages/app/services/storages/base_service.rb +++ b/modules/storages/app/services/storages/base_service.rb @@ -32,6 +32,7 @@ module Storages class BaseService extend ActiveModel::Naming extend ActiveModel::Translation + include Dry::Monads[:result] include TaggedLogging @@ -55,18 +56,28 @@ module Storages private + def add_validation_error(validation_error, options: {}) + log_validation_error(validation_error, options:) + + @result.errors.add(:base, :invalid, **validation_error.to_h) + @result.success = false + @result + end + # @param attribute [Symbol] attribute to which the error will be tied to - # @param storage_error [Storages::StorageError] an StorageError instance + # @param error [Storages::Adapters::Results::Error] An adapter error result # @param options [Hash{Symbol => Object}] optional extra parameters for the message generation # @return ServiceResult - def add_error(attribute, storage_error, options: {}) - case storage_error.code - when :error, :unauthorized - @result.errors.add(:base, storage_error.code, **options) + def add_error(attribute, error, options: {}) + log_adapter_error(error, options) + + if %i[error unauthorized not_found].include? error.code + @result.errors.add(:base, error.code, **options) else - @result.errors.add(attribute, storage_error.code, **options) + @result.errors.add(attribute, error.code, **options) end + @result.success = false @result end end diff --git a/modules/storages/app/services/storages/create_folder_service.rb b/modules/storages/app/services/storages/create_folder_service.rb index 7e7aa7815e2..4bf8c88af1e 100644 --- a/modules/storages/app/services/storages/create_folder_service.rb +++ b/modules/storages/app/services/storages/create_folder_service.rb @@ -33,45 +33,47 @@ module Storages using Peripherals::ServiceResultRefinements def self.call(storage:, user:, name:, parent_id:) - new.call(storage:, user:, name:, parent_id:) + new(storage).call(user:, name:, parent_id:) end - def call(storage:, user:, name:, parent_id:) - auth_strategy = Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user:, storage:) + def initialize(storage) + super() + @storage = storage + end - Peripherals::Registry - .resolve("#{storage}.commands.create_folder") - .call( - storage:, - auth_strategy:, - folder_name: name, - parent_location: parent_path(parent_id, storage, user) - ) + def call(user:, name:, parent_id:) + auth_strategy = Adapters::Registry.resolve("#{@storage}.authentication.user_bound").call(user, @storage) + parent_location = parent_path(parent_id, user).on_failure { return add_error(:base, it.errors) }.result + + input_data = Adapters::Input::CreateFolder.build(folder_name: name, parent_location:) + .value_or { return add_validation_error(it) } + + storage_folder = Adapters::Registry.resolve("#{@storage}.commands.create_folder") + .call(storage: @storage, auth_strategy:, input_data:) + .value_or { return add_error(:base, it, options: input_data.to_h) } + + @result.result = storage_folder + @result end private - def parent_path(parent_id, storage, user) - case storage.short_provider_type + def parent_path(parent_id, user) + case @storage.short_provider_type when "nextcloud" - location_from_file_info(parent_id, storage, user) + location_from_file_info(parent_id, user) when "one_drive" - Peripherals::ParentFolder.new(parent_id) + ServiceResult.success(result: parent_id) else raise "Unknown Storage Type" end end - def location_from_file_info(parent_id, storage, user) - StorageFileService - .call(storage: storage, user: user, file_id: parent_id) - .match( - on_success: lambda { |folder_info| - path = URI.decode_uri_component(folder_info.location) - Peripherals::ParentFolder.new(path) - }, - on_failure: ->(error) { raise error } - ) + def location_from_file_info(parent_id, user) + StorageFileService.call(storage: @storage, user:, file_id: parent_id).on_success do |success| + path = URI.decode_uri_component(success.result.location) + return ServiceResult.success(result: path) + end end end end diff --git a/modules/storages/app/services/storages/file_link_sync_service.rb b/modules/storages/app/services/storages/file_link_sync_service.rb index d3fcad46f77..b1042864a7b 100644 --- a/modules/storages/app/services/storages/file_link_sync_service.rb +++ b/modules/storages/app/services/storages/file_link_sync_service.rb @@ -30,8 +30,6 @@ module Storages class FileLinkSyncService < BaseService - using Peripherals::ServiceResultRefinements - def initialize(user:) super() @user = user @@ -41,14 +39,8 @@ module Storages with_tagged_logger do info "Starting File Link remote synchronization" - resulting_file_links = file_links - .group_by(&:storage_id) - .map { |storage_id, storage_file_links| sync_storage_data(storage_id, storage_file_links) } - .reduce([]) do |state, sync_result| - sync_result.match( - on_success: ->(sr) { state + sr }, - on_failure: ->(_) { state } - ) + resulting_file_links = file_links.group_by(&:storage).flat_map do |storage, records| + sync_storage_data(storage, records) end @result.result = resulting_file_links @@ -59,58 +51,49 @@ module Storages private - def sync_storage_data(storage_id, file_links) - storage = Storage.find(storage_id) - + def sync_storage_data(storage, file_links) info "Retrieving file link information from #{storage.name}" - Peripherals::Registry - .resolve("#{storage}.queries.files_info") - .call(storage:, auth_strategy: strategy(storage), file_ids: file_links.map(&:origin_id)) - .map { |file_infos| to_hash(file_infos) } - .match( - on_success: set_file_link_status(file_links), - on_failure: lambda { |_| - ServiceResult.success(result: file_links.map do |file_link| - file_link.origin_status = :error - file_link - end) - } - ) + + input_data = Adapters::Input::FilesInfo.build(file_ids: file_links.map(&:origin_id)) + .value_or { return add_validation_error(it) } + + infos = Adapters::Registry.resolve("#{storage}.queries.files_info") + .call(storage:, auth_strategy: auth_strategy(storage), input_data:) + + infos.either(->(success) { set_file_link_status(file_links, success) }, ->(*) { set_error_status(file_links) }) end - def strategy(storage) - Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user: @user, storage:) + def set_error_status(file_links) + file_links.map do |file_link| + file_link.origin_status = :error + file_link + end end - def to_hash(file_infos) - file_infos.index_by { |file_info| file_info.id.to_s }.to_h + def auth_strategy(storage) + Adapters::Registry.resolve("#{storage}.authentication.user_bound").call(@user, storage) end - def set_file_link_status(file_links) + def set_file_link_status(file_links, file_infos) info "Updating file link status..." - lambda do |file_infos| - resulting_file_links = [] + indexed = file_infos.index_by(&:id) - file_links.each do |file_link| - file_info = file_infos[file_link.origin_id] + file_links.map do |file_link| + file_info = indexed[file_link.origin_id] + file_link.origin_status = case file_info.status_code + when 200 + update_file_link(file_link, file_info) + :view_allowed + when 403 + :view_not_allowed + when 404 + :not_found + else + :error + end - file_link.origin_status = case file_info.status_code - when 200 - update_file_link(file_link, file_info) - :view_allowed - when 403 - :view_not_allowed - when 404 - :not_found - else - :error - end - - resulting_file_links << file_link - file_link.save - end - - ServiceResult.success(result: resulting_file_links) + file_link.save + file_link end end diff --git a/modules/storages/app/services/storages/file_links/copy_file_links_service.rb b/modules/storages/app/services/storages/file_links/copy_file_links_service.rb index 718b620c0ea..2b1ec6b4c70 100644 --- a/modules/storages/app/services/storages/file_links/copy_file_links_service.rb +++ b/modules/storages/app/services/storages/file_links/copy_file_links_service.rb @@ -30,8 +30,7 @@ module Storages module FileLinks - class CopyFileLinksService - include TaggedLogging + class CopyFileLinksService < BaseService include OpenProject::LocaleHelper def self.call(source:, target:, user:, work_packages_map:) @@ -39,6 +38,7 @@ module Storages end def initialize(source:, target:, user:, work_packages_map:) + super() @source = source @target = target @user = user @@ -55,80 +55,85 @@ module Storages info "Found #{source_file_links.count} source file links" with_locale_for(@user) do info "Creating file links..." - if @source.project_folder_automatic? - create_managed_file_links(source_file_links) - else - create_unmanaged_file_links(source_file_links) - end + copy_file_links(source_file_links) end end info "File link creation finished" + @result end private - # rubocop:disable Metrics/AbcSize - def create_managed_file_links(source_file_links) - info "Getting information about the source file links" - source_info = source_files_info(source_file_links).on_failure do |failed| - log_storage_error(failed.errors) - return failed - end - - info "Getting information about the copied target files" - target_map = target_files_map.on_failure do |failed| - log_storage_error(failed.errors) - return failed - end - - info "Building equivalency map..." - location_map = build_location_map(source_info.result, target_map.result) - - info "Creating file links based on the location map #{location_map}" - source_file_links.find_each do |source_link| - next if location_map[source_link.origin_id].blank? - - attributes = source_link.dup.attributes - attributes.merge!( - "storage_id" => @target.storage_id, - "creator_id" => @user.id, - "container_id" => @work_packages_map[source_link.container_id], - "origin_id" => location_map[source_link.origin_id] - ) - - CreateService.new(user: @user, contract_class: CopyContract) - .call(attributes).on_failure { |failed| log_errors(failed) } + def copy_file_links(source_file_links) + if @source.project_folder_automatic? + create_managed_file_links(source_file_links).or do |error| + log_adapter_error(error) + @result.success = false + end + else + create_unmanaged_file_links(source_file_links) end end - # rubocop:enable Metrics/AbcSize + + def create_managed_file_links(source_file_links) + info "Getting information about the source file links" + source_files_info(source_file_links).bind do |source_info| + info "Getting information about the copied target files" + target_files_map.bind do |target_map| + info "Building equivalency map..." + location_map = build_location_map(source_info, target_map) + + info "Creating file links based on the location map #{location_map}" + source_file_links.find_each do |source_link| + target_location = location_map[source_link.origin_id] + next if target_location.blank? + + create_target_file_link(source_link, target_location) + end + Success() + end + end + end + + def create_target_file_link(source_link, remote_id) + attributes = source_link.dup.attributes + attributes.merge!( + "storage_id" => @target.storage_id, + "creator_id" => @user.id, + "container_id" => @work_packages_map[source_link.container_id], + "origin_id" => remote_id + ) + + CreateService.new(user: @user, contract_class: CopyContract).call(attributes) + end def build_location_map(source_files, target_location_map) # We need this due to inconsistencies of how we represent the File Path target_location_map.transform_keys! { |key| key.starts_with?("/") ? key : "/#{key}" } - source_location_map = source_files.to_h { |info| [info.id.to_s, info.clean_location] } + source_files.to_h do |info| + target = info.clean_location.gsub(@source.managed_project_folder_path, @target.managed_project_folder_path) - source_location_map.each_with_object({}) do |(id, location), output| - target = location.gsub(@source.managed_project_folder_path, @target.managed_project_folder_path) - - output[id] = target_location_map[target]&.id || id + [info.id.to_s, target_location_map[target]&.id || id] end end def auth_strategy - Peripherals::Registry.resolve("#{@source.storage.short_provider_type}.authentication.userless").call + Adapters::Registry.resolve("#{@source.storage}.authentication.userless").call end def source_files_info(source_file_links) - Peripherals::Registry - .resolve("#{@source.storage.short_provider_type}.queries.files_info") - .call(storage: @source.storage, auth_strategy:, file_ids: source_file_links.pluck(:origin_id)) + Adapters::Input::FilesInfo.build(file_ids: source_file_links.pluck(:origin_id)).bind do |input_data| + Adapters::Registry.resolve("#{@source.storage}.queries.files_info") + .call(storage: @source.storage, auth_strategy:, input_data:) + end end def target_files_map - Peripherals::Registry - .resolve("#{@target.storage.short_provider_type}.queries.file_path_to_id_map") - .call(storage: @target.storage, auth_strategy:, folder: Peripherals::ParentFolder.new(@target.project_folder_location)) + Adapters::Input::FilePathToIdMap.build(folder: @target.project_folder_location).bind do |input_data| + Adapters::Registry.resolve("#{@target.storage}.queries.file_path_to_id_map") + .call(storage: @target.storage, auth_strategy:, input_data:) + end end def create_unmanaged_file_links(source_file_links) @@ -138,15 +143,9 @@ module Storages attributes["creator_id"] = @user.id attributes["container_id"] = @work_packages_map[source_file_link.container_id] - FileLinks::CreateService.new(user: @user, contract_class: CopyContract) - .call(attributes).on_failure { |failed| log_errors(failed) } + FileLinks::CreateService.new(user: @user, contract_class: CopyContract).call(attributes) end end - - def log_errors(failure) - error failure.inspect - error failure.errors.inspect - end end end end diff --git a/modules/storages/app/services/storages/health_service.rb b/modules/storages/app/services/storages/health_service.rb index d71c30d0fbb..7d15e11f308 100644 --- a/modules/storages/app/services/storages/health_service.rb +++ b/modules/storages/app/services/storages/health_service.rb @@ -53,7 +53,7 @@ module Storages time = Time.now.utc if @storage.health_status == "unhealthy" - if reason_is_same(reason) + if reason_is_same?(reason) @storage.update(health_checked_at: time) else @storage.update(health_changed_at: time, @@ -101,7 +101,7 @@ module Storages ::Storages::HealthStatusMailerJob.schedule(storage:) end - def reason_is_same(new_health_reason) + def reason_is_same?(new_health_reason) @storage.health_reason_identifier == ::Storages::Storage.extract_part_from_piped_string(new_health_reason, 0) end end diff --git a/modules/storages/app/services/storages/managed_folder_sync_service.rb b/modules/storages/app/services/storages/managed_folder_sync_service.rb index b4bdf2b1b17..045f984eb7b 100644 --- a/modules/storages/app/services/storages/managed_folder_sync_service.rb +++ b/modules/storages/app/services/storages/managed_folder_sync_service.rb @@ -72,11 +72,11 @@ module Storages end def folder_create_service - Peripherals::Registry.resolve("#{@storage}.services.folder_create") + Adapters::Registry.resolve("#{@storage}.services.upkeep_managed_folders") end def folder_permissions_service - Peripherals::Registry.resolve("#{@storage}.services.folder_permissions") + Adapters::Registry.resolve("#{@storage}.services.upkeep_managed_folder_permissions") end end end diff --git a/modules/storages/app/services/storages/nextcloud_managed_folder_create_service.rb b/modules/storages/app/services/storages/nextcloud_managed_folder_create_service.rb index 9bd61bfb891..d550aca9c7e 100644 --- a/modules/storages/app/services/storages/nextcloud_managed_folder_create_service.rb +++ b/modules/storages/app/services/storages/nextcloud_managed_folder_create_service.rb @@ -34,9 +34,11 @@ module Storages FILE_PERMISSIONS = OpenProject::Storages::Engine.external_file_permissions - def self.i18n_key = "nextcloud_sync_service" + delegate :group, :group_folder, :username, to: :@storage, private: true class << self + def i18n_key = "nextcloud_sync_service" + def call(storage:, project_storages_scope: nil) new(storage:, project_storages_scope:).call end @@ -48,133 +50,116 @@ module Storages @hide_missing_folders = project_storages_scope.nil? @project_storages = (project_storages_scope || storage.project_storages).active.automatic + setup_commands end def call with_tagged_logger([self.class.name, "storage-#{@storage.id}"]) do - prepare_remote_folders.on_failure { return epilogue } - epilogue + prepare_remote_folders + @result end end private - def epilogue - @result - end - - # @return [ServiceResult] - def prepare_remote_folders - info "Preparing the remote group folder #{@storage.group_folder}" - - remote_folders = remote_root_folder_map(@storage.group_folder).on_failure { return it }.result - info "Found #{remote_folders.count} remote folders" - - ensure_root_folder_permissions(remote_folders["/#{@storage.group_folder}"].id).on_failure { return it } - - ensure_folders_exist(remote_folders).on_success do - hide_inactive_folders(remote_folders) if @hide_missing_folders - end - end - # rubocop:disable Metrics/AbcSize - def hide_inactive_folders(remote_folders) - info "Hiding folders related to inactive projects" - project_folder_ids = @project_storages.pluck(:project_folder_id).compact + def prepare_remote_folders + info "Preparing the remote group folder #{group_folder}" - remote_folders.except("/#{@storage.group_folder}").each do |(path, file)| - folder_id = file.id + remote_root_folder_map.bind do |remote_folders| + info "Found #{remote_folders.count} remote folders" - next if project_folder_ids.include?(folder_id) - - info "Hiding folder #{folder_id} (#{path}) as it does not belong to any active project" - permissions = [ - { user_id: @storage.username, permissions: FILE_PERMISSIONS }, - { group_id: @storage.group, permissions: [] } - ] - - input_data = build_set_permissions_input_data(folder_id, permissions).value_or do |failure| - log_validation_error(failure, folder_id:, permissions:) - return # rubocop:disable Lint/NonLocalExitFromIterator - end - - set_permissions.call(storage: @storage, auth_strategy:, input_data:).on_failure do |service_result| - log_storage_error(service_result.errors, folder_id:, context: "hide_folder") - add_error(:hide_inactive_folders, service_result.errors, options: { folder_id: }) + root_folder = remote_folders.delete("/#{group_folder}") + ensure_root_folder_permissions(root_folder.id).bind do + ensure_folders_exist(remote_folders.invert).bind do + hide_inactive_folders(remote_folders.values.map(&:id)) + end end end end - # rubocop:enable Metrics/AbcSize + def hide_inactive_folders(existing_folder_ids) + info "Hiding inactive folders..." + user_permissions = [{ user_id: username, permissions: FILE_PERMISSIONS }, + { group_id: group, permissions: [] }] + active_folders = @project_storages.pluck(:project_folder_id) + + (existing_folder_ids - active_folders).each do |file_id| + Adapters::Input::SetPermissions.build(user_permissions:, file_id:).bind do |input_data| + @commands[:set_permissions].call(auth_strategy:, input_data:).or { |error| log_adapter_error(error) } + end + end + + Success(:hide_inactive_folders) + end + def ensure_folders_exist(remote_folders) info "Ensuring that automatically managed project folders exist and are correctly named." - id_folder_map = remote_folders.to_h { |path, file| [file.id, path] } + id_folder_map = remote_folders.transform_keys(&:id) - @project_storages.includes(:project).map do |project_storage| - unless id_folder_map.key?(project_storage.project_folder_id) - info "#{project_storage.managed_project_folder_path} does not exist. Creating..." - next create_remote_folder(project_storage) - end + @project_storages.includes(:project).find_each do |project_storage| + folder_id = project_storage.project_folder_id - rename_folder(project_storage, id_folder_map[project_storage.project_folder_id])&.on_failure { return it } + result = case id_folder_map[folder_id] + when nil + create_remote_folder(project_storage) + when project_storage.managed_project_folder_path.chop + Success() + else + rename_folder(folder_id, project_storage.managed_project_folder_name) + end + + result.or { return Failure() } end - # We processed every folder successfully - ServiceResult.success + Success(:setup_folders) end - # @param project_storage [Storages::ProjectStorage] Storages::ProjectStorage that the remote folder might need renaming - # @param current_path [String] current name of the remote project storage folder - # @return [ServiceResult, nil] - def rename_folder(project_storage, current_path) - return if UrlBuilder.path(current_path) == UrlBuilder.path(project_storage.managed_project_folder_path) + def rename_folder(location, new_name) + info "Renaming project folder to #{new_name}" - name = project_storage.managed_project_folder_name - file_id = project_storage.project_folder_id - - info "#{current_path} is misnamed. Renaming to #{name}" - rename_file.call(storage: @storage, auth_strategy:, file_id:, name:).on_failure do |service_result| - log_storage_error(service_result.errors, folder_id: file_id, folder_name: name) - - add_error(:rename_project_folder, service_result.errors, - options: { current_path:, project_folder_name: name, project_folder_id: file_id }).fail! + Adapters::Input::RenameFile.build(location:, new_name:).bind do |input_data| + @commands[:rename_file].call(auth_strategy:, input_data:).alt_map do |error| + add_error( + :rename_project_folder, error, + options: { current_path: location, project_folder_name: new_name } + ) + end end end def create_remote_folder(project_storage) folder_name = project_storage.managed_project_folder_path - parent_location = Peripherals::ParentFolder.new("/") - created_folder = create_folder.call(storage: @storage, auth_strategy:, folder_name:, parent_location:) - .on_failure do |service_result| - log_storage_error(service_result.errors, folder_name:) + input_data = Adapters::Input::CreateFolder.build(folder_name:, parent_location: "/").value_or do |error| + add_validation_error(error, options: { folder_id: folder_name }) + end - return add_error(:create_folder, service_result.errors, options: { folder_name:, parent_location: }) - end.result + created_folder = @commands[:create_folder].call(auth_strategy:, input_data:).value_or do |error| + add_error(:create_folder, error, options: { folder_name:, parent_location: "/" }) + return Failure() + end - last_project_folder = LastProjectFolder.find_or_initialize_by( - project_storage_id: project_storage.id, mode: project_storage.project_folder_mode - ) - - audit_last_project_folder(last_project_folder, created_folder.id) + audit_last_project_folder(project_storage, created_folder) end - def audit_last_project_folder(last_project_folder, project_folder_id) + def audit_last_project_folder(project_storage, created_folder) ApplicationRecord.transaction do - success = last_project_folder.update(origin_folder_id: project_folder_id) && - last_project_folder.project_storage.update(project_folder_id:) + last_project_folder = LastProjectFolder.find_or_initialize_by( + project_storage_id: project_storage.id, mode: project_storage.project_folder_mode + ) + + success = last_project_folder.update(origin_folder_id: created_folder.id) && + last_project_folder.project_storage.update(project_folder_id: created_folder.id) raise ActiveRecord::Rollback unless success end + + Success(:create_folder) end - # rubocop:disable Metrics/AbcSize - # @param root_folder_id [String] the id of the root folder - # @return [ServiceResult] def ensure_root_folder_permissions(root_folder_id) - username = @storage.username - group = @storage.group info "Setting needed permissions for user #{username} and group #{group} on the root group folder." permissions = [ { user_id: username, permissions: FILE_PERMISSIONS }, @@ -182,46 +167,42 @@ module Storages ] input_data = build_set_permissions_input_data(root_folder_id, permissions).value_or do |failure| - log_validation_error(failure, root_folder_id:, permissions:) - return ServiceResult.failure(result: failure.errors.to_h) # rubocop:disable Rails/DeprecatedActiveModelErrorsMethods + add_validation_error(failure, options: { root_folder_id:, permissions: }) + return Failure() end - set_permissions.call(storage: @storage, auth_strategy:, input_data:).on_failure do |service_result| - log_storage_error(service_result.errors, folder: "root", root_folder_id:) - add_error(:ensure_root_folder_permissions, service_result.errors, options: { group:, username: }).fail! + @commands[:set_permissions].call(auth_strategy:, input_data:).alt_map do |error| + add_error(:ensure_root_folder_permissions, error, options: { group:, username: }) end end - # rubocop:enable Metrics/AbcSize - - def remote_root_folder_map(group_folder) + def remote_root_folder_map info "Retrieving already existing folders under #{group_folder}" - file_path_to_id_map.call(storage: @storage, - auth_strategy:, - folder: Peripherals::ParentFolder.new(group_folder), - depth: 1) - .on_failure do |service_result| - log_storage_error(service_result.errors, { folder: group_folder }) - add_error(:remote_folders, service_result.errors, options: { group_folder:, username: @storage.username }).fail! + + input_data = Adapters::Input::FilePathToIdMap.build(folder: group_folder, depth: 1).value_or do |error| + add_validation_error(error, options: { folder: group_folder }) + + return Failure() + end + + @commands[:file_path_to_id_map].call(auth_strategy:, input_data:).alt_map do |error| + add_error(:remote_folders, error, options: { group_folder:, username: }) end end def build_set_permissions_input_data(file_id, user_permissions) - Peripherals::StorageInteraction::Inputs::SetPermissions.build(file_id:, user_permissions:) + Adapters::Input::SetPermissions.build(file_id:, user_permissions:) end - def create_folder = Peripherals::Registry.resolve("nextcloud.commands.create_folder") - - def rename_file = Peripherals::Registry.resolve("nextcloud.commands.rename_file") - - def set_permissions = Peripherals::Registry.resolve("nextcloud.commands.set_permissions") - - def file_path_to_id_map = Peripherals::Registry.resolve("nextcloud.queries.file_path_to_id_map") - - def userless = Peripherals::Registry.resolve("nextcloud.authentication.userless") - def auth_strategy - @auth_strategy ||= userless.call + @auth_strategy ||= Adapters::Registry["nextcloud.authentication.userless"].call + end + + def setup_commands + @commands = %w[nextcloud.commands.create_folder nextcloud.commands.rename_file nextcloud.commands.set_permissions + nextcloud.queries.file_path_to_id_map].each_with_object({}) do |key, hash| + hash[key.split(".").last.to_sym] = Adapters::Registry[key].new(@storage) + end end end end diff --git a/modules/storages/app/services/storages/nextcloud_managed_folder_permissions_service.rb b/modules/storages/app/services/storages/nextcloud_managed_folder_permissions_service.rb index 7364942ea34..4695388eb5c 100644 --- a/modules/storages/app/services/storages/nextcloud_managed_folder_permissions_service.rb +++ b/modules/storages/app/services/storages/nextcloud_managed_folder_permissions_service.rb @@ -34,57 +34,46 @@ module Storages FILE_PERMISSIONS = OpenProject::Storages::Engine.external_file_permissions - def self.i18n_key = "nextcloud_sync_service" - class << self + def i18n_key = "nextcloud_sync_service" + def call(storage:, project_storages_scope: nil) new(storage:, project_storages_scope:).call end end + delegate :group_user, :group, :username, to: :@storage, private: true + def initialize(storage:, project_storages_scope: nil) super() @storage = storage @project_storages = project_storages_scope || storage.project_storages + setup_commands end def call with_tagged_logger([self.class.name, "storage-#{@storage.id}"]) do - apply_permissions_to_folders - epilogue + apply_permissions_to_folders.bind { add_remove_users_to_group } + @result end end private - def epilogue - @result - end - def apply_permissions_to_folders info "Setting permissions to project folders" remote_admins = admin_remote_identities.pluck(:origin_user_id) - @project_storages - .active - .automatic - .with_project_folder - .order(:project_folder_id) - .find_each do |project_storage| - set_folder_permissions(remote_admins, project_storage) - end + @project_storages.active.automatic.with_project_folder.order(:project_folder_id).find_each do |project_storage| + set_folder_permissions(remote_admins, project_storage) + end - info "Updating user access on automatically managed project folders" - add_remove_users_to_group(@storage.group, @storage.username) - - ServiceResult.success + Success(:folder_permissions) end - def add_remove_users_to_group(group, username) - remote_users = remote_group_users.result_or do |error| - log_storage_error(error, group:) - return add_error(:remote_group_users, error, options: { group: }).fail! - end + def add_remove_users_to_group + info "Updating user access on automatically managed project folders" + remote_users = remote_group_users.value_or { return Failure() } local_users = remote_identities.order(:id).pluck(:origin_user_id) @@ -93,56 +82,58 @@ module Storages end def add_users_to_remote_group(users_to_add) - group = @storage.group - users_to_add.each do |user| - add_user_to_group.call(storage: @storage, auth_strategy:, user:, group:).error_and do |error| - add_error(:add_user_to_group, error, options: { user:, group:, reason: error.log_message }) - log_storage_error(error, group:, user:, reason: error.log_message) + input_data = Adapters::Input::AddUserToGroup.build(group:, user:).value_or do |error| + next add_validation_error(error) + end + + @commands[:add_user_to_group].call(auth_strategy:, input_data:).or do |error| + add_error(:add_user_to_group, error, options: { user:, group: }) end end end def remove_users_from_remote_group(users_to_remove) - group = @storage.group - users_to_remove.each do |user| - remove_user_from_group.call(storage: @storage, auth_strategy:, user:, group:).error_and do |error| - add_error(:remove_user_from_group, error, options: { user:, group:, reason: error.log_message }) - log_storage_error(error, group:, user:, reason: error.log_message) + input_data = Adapters::Input::RemoveUserFromGroup.build(group:, user:).value_or do |error| + add_validation_error(error, options: { user:, group: }) + next + end + + @commands[:remove_user_from_group].call(auth_strategy:, input_data:).or do |error| + add_error(:remove_user_from_group, error, options: { user:, group:, reason: error.code }) end end end # rubocop:disable Metrics/AbcSize def set_folder_permissions(remote_admins, project_storage) - system_user = [{ user_id: @storage.username, permissions: FILE_PERMISSIONS }] - admin_permissions = remote_admins.to_set.map { |username| { user_id: username, permissions: FILE_PERMISSIONS } } + base_permissions = base_remote_permissions(admin_permissions) users_permissions = project_remote_identities(project_storage).map do |identity| - permissions = identity.user.all_permissions_for(project_storage.project) & FILE_PERMISSIONS - { user_id: identity.origin_user_id, permissions: } + { user_id: identity.origin_user_id, + permissions: identity.user.all_permissions_for(project_storage.project) & FILE_PERMISSIONS } end - group_permissions = [{ group_id: @storage.group, permissions: [] }] - - permissions = system_user + admin_permissions + users_permissions + group_permissions + permissions = base_permissions + users_permissions project_folder_id = project_storage.project_folder_id input_data = build_set_permissions_input_data(project_folder_id, permissions).value_or do |failure| log_validation_error(failure, project_folder_id:, permissions:) - return # rubocop:disable Lint/NonLocalExitFromIterator end - set_permissions.call(storage: @storage, auth_strategy:, input_data:).on_failure do |service_result| - log_storage_error(service_result.errors, folder: project_folder_id) - add_error(:set_folder_permission, service_result.errors, options: { folder: project_folder_id }) + @commands[:set_permissions].call(auth_strategy:, input_data:).or do |error| + add_error(:set_folder_permission, error, options: { folder: project_folder_id }) end end - # rubocop:enable Metrics/AbcSize + def base_remote_permissions(admin_permissions) + [{ user_id: @storage.username, permissions: FILE_PERMISSIONS }, + { group_id: @storage.group, permissions: [] }] + admin_permissions + end + def project_remote_identities(project_storage) user_remote_identities = remote_identities.where.not(id: admin_remote_identities).order(:id) @@ -154,12 +145,18 @@ module Storages end def build_set_permissions_input_data(file_id, user_permissions) - Peripherals::StorageInteraction::Inputs::SetPermissions.build(file_id:, user_permissions:) + Adapters::Input::SetPermissions.build(file_id:, user_permissions:) end def remote_group_users - info "Retrieving users that are part of the #{@storage.group} group" - group_users.call(storage: @storage, auth_strategy:, group: @storage.group) + info "Retrieving users that are part of the #{group} group" + input_data = Adapters::Input::GroupUsers + .build(group:) + .value_or { return Failure(add_validation_error(it, options: { group: })) } + + @commands[:group_users].call(auth_strategy:, input_data:).or do |error| + Failure(add_error(:group_users, error, options: { group: group })) + end end ### Model Scopes @@ -172,18 +169,15 @@ module Storages remote_identities.where(user: User.admin.active) end - def set_permissions = Peripherals::Registry.resolve("nextcloud.commands.set_permissions") - - def add_user_to_group = Peripherals::Registry.resolve("nextcloud.commands.add_user_to_group") - - def remove_user_from_group = Peripherals::Registry.resolve("nextcloud.commands.remove_user_from_group") - - def group_users = Peripherals::Registry.resolve("nextcloud.queries.group_users") - - def userless = Peripherals::Registry.resolve("nextcloud.authentication.userless") - def auth_strategy - @auth_strategy ||= userless.call + @auth_strategy ||= Adapters::Registry.resolve("nextcloud.authentication.userless").call + end + + def setup_commands + @commands = %w[nextcloud.commands.set_permissions nextcloud.commands.remove_user_from_group + nextcloud.commands.add_user_to_group nextcloud.queries.group_users].to_h do |key| + [key.split(".").last.to_sym, Adapters::Registry[key].new(@storage)] + end end end end diff --git a/modules/storages/app/services/storages/one_drive_managed_folder_create_service.rb b/modules/storages/app/services/storages/one_drive_managed_folder_create_service.rb index 3d77b954aff..1431ae80f9e 100644 --- a/modules/storages/app/services/storages/one_drive_managed_folder_create_service.rb +++ b/modules/storages/app/services/storages/one_drive_managed_folder_create_service.rb @@ -49,10 +49,10 @@ module Storages def call with_tagged_logger([self.class.name, "storage-#{@storage.id}"]) do - existing_remote_folders = remote_folders_map(@storage.drive_id).on_failure { return @result }.result - - ensure_folders_exist(existing_remote_folders).on_success do - hide_inactive_folders(existing_remote_folders) if @hide_missing_folders + remote_folders_map(@storage.drive_id).bind do |existing_remote_folders| + ensure_folders_exist(existing_remote_folders).bind do + hide_inactive_folders(existing_remote_folders) if @hide_missing_folders + end end @result @@ -72,25 +72,23 @@ module Storages rename_project_folder(folder_map[project_storage.project_folder_id], project_storage) end - ServiceResult.success(result: "folders processed") + Success(:folder_maintenance_done) end def hide_inactive_folders(folder_map) info "Hiding folders related to inactive projects" - inactive_folder_ids(folder_map).each { |item_id| hide_folder(item_id) } + inactive_folder_ids(folder_map).each { |item_id| hide_folder(item_id, folder_map) } end - def hide_folder(item_id) + def hide_folder(item_id, folder_map) info "Hiding folder with ID #{item_id} as it does not belong to any active project" build_permissions_input_data(item_id, []) .either( ->(input_data) do - set_permissions.call(storage: @storage, auth_strategy:, input_data:) - .on_failure do |service_result| - log_storage_error(service_result.errors, item_id:, context: "hide_folder") - add_error(:hide_inactive_folders, service_result.errors, options: { path: folder_map[item_id] }) + set_permissions.call(auth_strategy:, input_data:).or do |error| + add_error(:hide_inactive_folders, error, options: { context: "hide folders", path: folder_map[item_id] }) end end, ->(failure) { log_validation_error(failure, item_id:, context: "hide_folder") } @@ -107,25 +105,28 @@ module Storages info "#{current_folder_name} is misnamed. Renaming to #{actual_path}" folder_id = project_storage.project_folder_id - rename_file.call(storage: @storage, auth_strategy:, file_id: folder_id, name: actual_path) - .on_failure do |service_result| - log_storage_error(service_result.errors, folder_id:, folder_name: actual_path) - add_error( - :rename_project_folder, service_result.errors, - options: { current_path: current_folder_name, project_folder_name: actual_path, project_folder_id: folder_id } - ) + Adapters::Input::RenameFile.build(location: folder_id, new_name: actual_path).bind do |input_data| + rename_file.call(auth_strategy:, input_data:).or do |error| + add_error( + :rename_project_folder, error, + options: { current_path: current_folder_name, project_folder_name: actual_path, project_folder_id: folder_id } + ) + end end end def create_remote_folder(folder_name, project_storage_id) - folder_info = create_folder.call(storage: @storage, auth_strategy:, folder_name:, parent_location: root_folder) - .on_failure do |service_result| - log_storage_error(service_result.errors, folder_name:) - return add_error(:create_folder, service_result.errors, options: { folder_name:, parent_location: root_folder }) - end.result + input_data = Adapters::Input::CreateFolder + .build(folder_name:, parent_location: "/") + .value_or { return Failure(log_validation_error(it, folder_name: folder_name, parent_location: "/")) } - last_project_folder = ::Storages::LastProjectFolder.find_by(project_storage_id:, mode: :automatic) + folder_info = create_folder.call(auth_strategy:, input_data:).value_or do |error| + add_error(:create_folder, error, options: { folder_name:, parent_location: "/" }) + return Failure() + end + + last_project_folder = LastProjectFolder.find_by(project_storage_id:, mode: :automatic) audit_last_project_folder(last_project_folder, folder_info.id) end @@ -143,44 +144,41 @@ module Storages def remote_folders_map(drive_id) info "Retrieving already existing folders under #{drive_id}" - file_list = files.call(storage: @storage, auth_strategy:, folder: root_folder).on_failure do |failed| - log_storage_error(failed.errors, { drive_id: }) - return add_error(:remote_folders, failed.errors, options: { drive_id: }).fail! - end.result - - ServiceResult.success(result: filter_folders_from(file_list.files)) - end - - # @param files [Array] - # @return Hash{String => String} a hash of item ID and item name. - def filter_folders_from(files) - folders = files.each_with_object({}) do |file, hash| - next unless file.folder? - - hash[file.id] = file.name + input_data = Adapters::Input::Files.build(folder: "/").value_or do |it| + log_validation_error(it, context: "remote_folders") + return Failure() end - info "Found #{folders.size} folders. #{folders}" + file_list = files.call(auth_strategy:, input_data:).value_or do |error| + add_error(:remote_folders, error, options: { drive_id: }) + return Failure() + end - folders + filter_folders_from(file_list) end - def root_folder = Peripherals::ParentFolder.new("/") + def filter_folders_from(files) + folder_map = files.all_folders.to_h { [it.id, it.name] } + info "Found #{folder_map.size} folders. Map: #{folder_map}" + Success(folder_map) + end - def create_folder = Peripherals::Registry.resolve("one_drive.commands.create_folder") + def root_folder = "/" - def rename_file = Peripherals::Registry.resolve("one_drive.commands.rename_file") + def create_folder = Adapters::Registry.resolve("one_drive.commands.create_folder").new(@storage) - def set_permissions = Peripherals::Registry.resolve("one_drive.commands.set_permissions") + def rename_file = Adapters::Registry.resolve("one_drive.commands.rename_file").new(@storage) - def files = Peripherals::Registry.resolve("one_drive.queries.files") + def set_permissions = Adapters::Registry.resolve("one_drive.commands.set_permissions").new(@storage) - def userless = Peripherals::Registry.resolve("one_drive.authentication.userless") - - def auth_strategy = userless.call + def files = Adapters::Registry.resolve("one_drive.queries.files").new(@storage) def build_permissions_input_data(file_id, user_permissions) - Peripherals::StorageInteraction::Inputs::SetPermissions.build(file_id:, user_permissions:) + Adapters::Input::SetPermissions.build(file_id:, user_permissions:) + end + + def auth_strategy + @auth_strategy ||= Adapters::Registry["one_drive.authentication.userless"].call end end end diff --git a/modules/storages/app/services/storages/one_drive_managed_folder_permissions_service.rb b/modules/storages/app/services/storages/one_drive_managed_folder_permissions_service.rb index 7e8b80319ee..fd7a8861d84 100644 --- a/modules/storages/app/services/storages/one_drive_managed_folder_permissions_service.rb +++ b/modules/storages/app/services/storages/one_drive_managed_folder_permissions_service.rb @@ -61,12 +61,8 @@ module Storages # rubocop:disable Metrics/AbcSize def apply_permission_to_folders info "Setting permissions to project folders" - @project_storages.includes(:project) - .with_project_folder - .find_each do |project_storage| - permissions = admin_remote_identities_scope - .pluck(:origin_user_id) - .map do |origin_user_id| + @project_storages.includes(:project).with_project_folder.find_each do |project_storage| + permissions = admin_remote_identities_scope.pluck(:origin_user_id).map do |origin_user_id| { user_id: origin_user_id, permissions: [:write_files] } end @@ -115,14 +111,11 @@ module Storages client_remote_identities_scope.where(user: User.admin.active) end - def set_permissions = Peripherals::Registry.resolve("one_drive.commands.set_permissions") - - def userless = Peripherals::Registry.resolve("one_drive.authentication.userless") - - def auth_strategy = userless.call + def set_permissions = Adapters::Registry.resolve("one_drive.commands.set_permissions") + def auth_strategy = Adapters::Registry.resolve("one_drive.authentication.userless").call def build_permissions_input_data(file_id, user_permissions) - Peripherals::StorageInteraction::Inputs::SetPermissions.build(file_id:, user_permissions:) + Adapters::Input::SetPermissions.build(file_id:, user_permissions:) end end end diff --git a/modules/storages/app/services/storages/project_storages/copy_project_folders_service.rb b/modules/storages/app/services/storages/project_storages/copy_project_folders_service.rb index 3b3e4caae62..283746cd6fb 100644 --- a/modules/storages/app/services/storages/project_storages/copy_project_folders_service.rb +++ b/modules/storages/app/services/storages/project_storages/copy_project_folders_service.rb @@ -39,11 +39,10 @@ module Storages def initialize super - @data = Peripherals::StorageInteraction::ResultData::CopyTemplateFolder - .new(id: nil, polling_url: nil, requires_polling: false) + @data = Adapters::Results::CopyTemplateFolder.new(id: nil, polling_url: nil, requires_polling: false) end - # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize def call(source, target) with_tagged_logger([self.class, source&.id, target&.id]) do return @result.map { @data } if non_managed_project_folder?(source) @@ -51,10 +50,13 @@ module Storages info "Initiating copy of project folder #{source.managed_project_folder_path} to #{target.managed_project_folder_path}" copy_result = initiate_copy(source.storage, source.project_folder_location, target.managed_project_folder_path) - @result.map { copy_result.result } + copy_result.either( + ->(success) { @result.map { success } }, + ->(failed) { @result.map { failed } } + ) end end - # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/PerceivedComplexity, Metrics/AbcSize private @@ -73,19 +75,18 @@ module Storages end def initiate_copy(storage, source_path, destination_path) - Peripherals::Registry - .resolve("#{storage}.commands.copy_template_folder") - .call(auth_strategy: auth_strategy(storage.short_provider_type), - storage:, - source_path:, - destination_path:).on_failure do |failed| - log_storage_error(failed.errors) - add_error(:base, failed.errors, options: { destination_path:, source_path: }).fail! + Adapters::Input::CopyTemplateFolder.build(source: source_path, destination: destination_path).bind do |input_data| + Adapters::Registry + .resolve("#{storage}.commands.copy_template_folder") + .call(auth_strategy: auth_strategy(storage), storage:, input_data:).alt_map do |failed| + log_adapter_error(failed) + add_error(:base, failed, options: { destination_path:, source_path: }) + end end end - def auth_strategy(short_provider_type) - Peripherals::Registry.resolve("#{short_provider_type}.authentication.userless").call + def auth_strategy(storage) + Adapters::Registry.resolve("#{storage}.authentication.userless").call end end end diff --git a/modules/storages/app/services/storages/project_storages/delete_service.rb b/modules/storages/app/services/storages/project_storages/delete_service.rb index b7608229b90..a518c730bb4 100644 --- a/modules/storages/app/services/storages/project_storages/delete_service.rb +++ b/modules/storages/app/services/storages/project_storages/delete_service.rb @@ -28,59 +28,60 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Storages::ProjectStorages - # Performs the deletion in the superclass. Associated FileLinks are deleted - # by the model before_destroy hook. - class DeleteService < ::BaseServices::Delete - def before_perform(*) - delete_project_folder if model.project_folder_automatic? +module Storages + module ProjectStorages + # Performs the deletion in the superclass. Associated FileLinks are deleted + # by the model before_destroy hook. + class DeleteService < ::BaseServices::Delete + def before_perform(*) + delete_project_folder if model.project_folder_automatic? - super - end + super + end - # "persist" is a callback from BaseContracted.perform - # that is supposed to do the actual work in a contract. - # So in a DeleteService it performs the actual delete, - # except for the @object.destroy that is already performed - # by ::BaseServices::Delete - def persist(service_result) - # Perform the @object.destroy etc. in the super-class - super.tap do |deletion_result| - if deletion_result.success? - delete_associated_file_links - ::Storages::ProjectStorages::NotificationsService.broadcast_project_storage_destroyed( - project_storage: deletion_result.result - ) + # "persist" is a callback from BaseContracted.perform + # that is supposed to do the actual work in a contract. + # So in a DeleteService it performs the actual delete, + # except for the @object.destroy that is already performed + # by ::BaseServices::Delete + def persist(service_result) + # Perform the @object.destroy etc. in the super-class + super.tap do |deletion_result| + if deletion_result.success? + delete_associated_file_links + NotificationsService.broadcast_project_storage_destroyed( + project_storage: deletion_result.result + ) + end end end - end - private + private - def auth_strategy - ::Storages::Peripherals::Registry - .resolve("#{model.storage.short_provider_type}.authentication.userless") - .call - end + def auth_strategy + Adapters::Registry.resolve("#{model.storage}.authentication.userless").call + end - def delete_project_folder - ::Storages::Peripherals::Registry - .resolve("#{model.storage.short_provider_type}.commands.delete_folder") - .call(storage: model.storage, auth_strategy:, location: model.project_folder_location) - end + def delete_project_folder + Adapters::Input::DeleteFolder.build(location: model.project_folder_location).bind do |input_data| + Adapters::Registry.resolve("#{model.storage}.commands.delete_folder") + .call(storage: model.storage, auth_strategy:, input_data:) + end + end - # Delete FileLinks with the same Storage as the ProjectStorage. - # Also, they are attached to WorkPackages via the Project. - def delete_associated_file_links - # work_packages is an ActiveRecord::Relation, not an array of objects! - work_packages = WorkPackage.where(project_id: model.project_id) - file_links = Storages::FileLink.where(storage_id: model.storage_id, - container: work_packages) - # use file_links.to_sql to check the SQL generated by the lines above. - # It uses a fast SQL "container_id in (select * from work_packages...)", so the - # delete_all below is executed using a single query. - # Reference: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html - file_links.delete_all + # Delete FileLinks with the same Storage as the ProjectStorage. + # Also, they are attached to WorkPackages via the Project. + def delete_associated_file_links + # work_packages is an ActiveRecord::Relation, not an array of objects! + work_packages = WorkPackage.where(project_id: model.project_id) + file_links = FileLink.where(storage_id: model.storage_id, + container: work_packages) + # use file_links.to_sql to check the SQL generated by the lines above. + # It uses a fast SQL "container_id in (select * from work_packages...)", so the + # delete_all below is executed using a single query. + # Reference: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html + file_links.delete_all + end end end end diff --git a/modules/storages/app/services/storages/storage_file_service.rb b/modules/storages/app/services/storages/storage_file_service.rb index ed8429fdd7f..ad9e9de7ac3 100644 --- a/modules/storages/app/services/storages/storage_file_service.rb +++ b/modules/storages/app/services/storages/storage_file_service.rb @@ -35,16 +35,16 @@ module Storages end def call(user:, storage:, file_id:) - auth_strategy = strategy(storage, user) + auth_strategy = Adapters::Registry.resolve("#{storage}.authentication.user_bound").call(user, storage) info "Requesting file #{file_id} information on #{storage.name}" - Peripherals::Registry.resolve("#{storage}.queries.file_info").call(storage:, auth_strategy:, file_id:) - end + input_data = Adapters::Input::FileInfo.build(file_id:).value_or { return add_validation_error(it) } - private + file_info = Adapters::Registry.resolve("#{storage}.queries.file_info").call(storage:, auth_strategy:, input_data:) + .value_or { return add_error(:base, it, options: { file_id: }) } - def strategy(storage, user) - Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user:, storage:) + @result.result = file_info + @result end end end diff --git a/modules/storages/app/services/storages/storage_files_service.rb b/modules/storages/app/services/storages/storage_files_service.rb index be0e9ba9f26..9b8b7deb702 100644 --- a/modules/storages/app/services/storages/storage_files_service.rb +++ b/modules/storages/app/services/storages/storage_files_service.rb @@ -35,16 +35,27 @@ module Storages end def call(user:, storage:, folder:) - auth_strategy = strategy(storage, user) + with_tagged_logger do + auth_strategy = strategy(storage, user) - info "Requesting all the files under folder #{folder} for #{storage.name}" - Peripherals::Registry.resolve("#{storage}.queries.files").call(storage:, auth_strategy:, folder:) + info "Requesting all the files under folder #{folder} for #{storage.name}" + info "auth_strategy: #{auth_strategy}" + + input_data = Adapters::Input::Files.build(folder:).value_or { return add_validation_error(it) } + + files = Adapters::Registry + .resolve("#{storage}.queries.files").call(storage:, auth_strategy:, input_data:) + .value_or { return add_error(:base, it, options: { storage_name: storage.name, folder: }) } + + @result.result = files + @result + end end private def strategy(storage, user) - Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user:, storage:) + Adapters::Registry.resolve("#{storage}.authentication.user_bound").call(user, storage) end end end diff --git a/modules/storages/app/services/storages/upload_link_service.rb b/modules/storages/app/services/storages/upload_link_service.rb index 864921ea389..bb874031397 100644 --- a/modules/storages/app/services/storages/upload_link_service.rb +++ b/modules/storages/app/services/storages/upload_link_service.rb @@ -46,7 +46,7 @@ module Storages info "Upload data validated..." info "Requesting an upload link to #{@storage.name}" - upload_link = request_upload_link(auth_strategy(user), input).on_failure { return @result }.result + upload_link = request_upload_link(auth_strategy(user), input).value_or { return @result } @result.result = upload_link @result @@ -55,18 +55,17 @@ module Storages private - def request_upload_link(auth_strategy, upload_data) - Peripherals::Registry.resolve("#{@storage}.queries.upload_link") - .call(storage: @storage, auth_strategy:, upload_data:) - .on_failure do |error| - add_error(:base, error.errors, options: { storage_name: @storage.name, folder: upload_data.folder_id }) - log_storage_error(error.errors) + def request_upload_link(auth_strategy, input_data) + Adapters::Registry.resolve("#{@storage}.queries.upload_link") + .call(storage: @storage, auth_strategy:, input_data:) + .alt_map do |error| + add_error(:base, error, options: { storage_name: @storage.name, folder: input_data.folder_id }) @result.success = false end end def validate_input(...) - Peripherals::StorageInteraction::Inputs::UploadData.build(...).alt_map do |failure| + Adapters::Input::UploadLink.build(...).alt_map do |failure| error failure.inspect @result.add_error(:base, :invalid, options: failure.to_h) @result.success = false @@ -74,7 +73,7 @@ module Storages end def auth_strategy(user) - Peripherals::Registry.resolve("#{@storage}.authentication.user_bound").call(user:, storage: @storage) + Adapters::Registry.resolve("#{@storage}.authentication.user_bound").call(user, @storage) end end end diff --git a/modules/storages/app/workers/storages/copy_project_folders_job.rb b/modules/storages/app/workers/storages/copy_project_folders_job.rb index 4ef5f6c0fd4..c55cdaa87e2 100644 --- a/modules/storages/app/workers/storages/copy_project_folders_job.rb +++ b/modules/storages/app/workers/storages/copy_project_folders_job.rb @@ -56,9 +56,8 @@ module Storages private def initiate_copy(target) - ProjectStorages::CopyProjectFoldersService - .call(source: @source, target:) - .on_success { |success| prepare_polling(success.result) } + ProjectStorages::CopyProjectFoldersService.call(source: @source, target:) + .on_success { |success| prepare_polling(success.result) } end def prepare_polling(result) @@ -80,7 +79,7 @@ module Storages polling_info[:polling_state] = :completed batch.save - result = Peripherals::StorageInteraction::ResultData::CopyTemplateFolder.new(response[:resourceId], nil, false) + result = Adapters::Results::CopyTemplateFolder.new(response[:resourceId], nil, false) ServiceResult.success(result:) else raise(Errors::PollingRequired, "#{job_id} Polling not completed yet") diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index 0d0954ff703..75241f01a65 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -143,6 +143,7 @@ en: conflict: 'The user %{user} could not be removed from the %{group} group for the following reason: %{reason}' failed_to_remove: 'The user %{user} could not be removed from the %{group} group for the following reason: %{reason}' rename_project_folder: + conflict: OpenProject could not rename the project folder to %{current_path} as a folder with the same name already exists forbidden: OpenProject user does not have access to %{current_path} folder. not_found: "%{current_path} wasn't found." set_folders_permissions: diff --git a/modules/storages/lib/api/v3/file_links/file_links_download_api.rb b/modules/storages/lib/api/v3/file_links/file_links_download_api.rb index 788fbddd9f1..24815bfbcdc 100644 --- a/modules/storages/lib/api/v3/file_links/file_links_download_api.rb +++ b/modules/storages/lib/api/v3/file_links/file_links_download_api.rb @@ -35,22 +35,23 @@ class API::V3::FileLinks::FileLinksDownloadAPI < API::OpenProjectAPI helpers do def auth_strategy storage = @file_link.storage - Storages::Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user: User.current, storage:) + Storages::Adapters::Registry.resolve("#{storage}.authentication.user_bound").call(current_user, storage) end end resources :download do get do - Storages::Peripherals::Registry - .resolve("#{@file_link.storage}.queries.download_link") - .call(storage: @file_link.storage, auth_strategy:, file_link: @file_link) - .match( - on_success: ->(url) do - redirect(url, body: "The requested resource can be downloaded from #{url}") - status(303) - end, - on_failure: ->(error) { raise_error(error) } - ) + Storages::Adapters::Input::DownloadLink.build(file_link: @file_link).bind do |input_data| + Storages::Adapters::Registry.resolve("#{@file_link.storage}.queries.download_link") + .call(storage: @file_link.storage, auth_strategy:, input_data:) + .either( + ->(url) do + redirect(url, body: "The requested resource can be downloaded from #{url}") + status(303) + end, + ->(error) { raise_error(error) } + ) + end end end end diff --git a/modules/storages/lib/api/v3/file_links/file_links_open_api.rb b/modules/storages/lib/api/v3/file_links/file_links_open_api.rb index 99eea584d9d..7ab72152513 100644 --- a/modules/storages/lib/api/v3/file_links/file_links_open_api.rb +++ b/modules/storages/lib/api/v3/file_links/file_links_open_api.rb @@ -31,31 +31,27 @@ class API::V3::FileLinks::FileLinksOpenAPI < API::OpenProjectAPI helpers Storages::Peripherals::StorageErrorHelper - using Storages::Peripherals::ServiceResultRefinements - helpers do def auth_strategy storage = @file_link.storage - Storages::Peripherals::Registry.resolve("#{storage}.authentication.user_bound").call(user: current_user, storage:) + Storages::Adapters::Registry.resolve("#{storage}.authentication.user_bound").call(current_user, storage) end end resources :open do get do - Storages::Peripherals::Registry - .resolve("#{@file_link.storage}.queries.open_file_link") - .call( - storage: @file_link.storage, - auth_strategy:, - file_id: @file_link.origin_id, - open_location: ActiveModel::Type::Boolean.new.cast(params[:location]) - ) - .match( - on_success: ->(url) do + input_data = Storages::Adapters::Input::OpenFileLink + .build(file_id: @file_link.origin_id, open_location: params[:location]) + .value_or { raise_error(it) } + + Storages::Adapters::Registry.resolve("#{@file_link.storage}.queries.open_file_link") + .call(storage: @file_link.storage, auth_strategy:, input_data:) + .either( + ->(url) do redirect url, body: "The requested resource can be viewed at #{url}" status 303 end, - on_failure: ->(error) { raise_error(error) } + ->(error) { raise_error(error) } ) end end diff --git a/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb b/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb index efdae12785f..7efaac7b2c0 100644 --- a/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb +++ b/modules/storages/lib/api/v3/file_links/work_packages_file_links_api.rb @@ -36,11 +36,7 @@ module API def sync_and_convert_relation(file_links) return ::Storages::FileLink.none if file_links.empty? - sync_result = ::Storages::FileLinkSyncService - .new(user: current_user) - .call(file_links) - .result - + sync_result = ::Storages::FileLinkSyncService.new(user: current_user).call(file_links).result id_status_map = {} sync_result.each do |file_link| @@ -53,11 +49,11 @@ module API resources :file_links do get do - query = ParamsToQueryService - .new(::Storages::Storage, - current_user, - query_class: ::Queries::Storages::FileLinks::FileLinkQuery) - .call(params) + query = ParamsToQueryService.new( + ::Storages::Storage, + current_user, + query_class: ::Queries::Storages::FileLinks::FileLinkQuery + ).call(params) unless query.valid? message = I18n.t("api_v3.errors.missing_or_malformed_parameter", parameter: "filters") diff --git a/modules/storages/lib/api/v3/storage_files/storage_files_api.rb b/modules/storages/lib/api/v3/storage_files/storage_files_api.rb index 5c92deabb1a..5466f2e9a5f 100644 --- a/modules/storages/lib/api/v3/storage_files/storage_files_api.rb +++ b/modules/storages/lib/api/v3/storage_files/storage_files_api.rb @@ -55,20 +55,15 @@ module API::V3::StorageFiles Storages::UploadLinkService.call(storage: @storage, upload_data:, user: current_user) end end - - def auth_strategy - Storages::Peripherals::Registry.resolve("#{@storage}.authentication.user_bound") - .call(user: current_user, storage: @storage) - end end resources :files do get do Storages::StorageFilesService - .call(storage: @storage, user: current_user, folder: extract_parent_folder(params)) + .call(storage: @storage, user: current_user, folder: params.fetch(:parent, "/")) .match( on_success: ->(files) { API::V3::StorageFiles::StorageFilesRepresenter.new(files, @storage, current_user:) }, - on_failure: ->(error) { raise_error(error) } + on_failure: ->(error) { raise_service_result_error(error) } ) end @@ -81,7 +76,7 @@ module API::V3::StorageFiles on_success: lambda { |storage_file| API::V3::StorageFiles::StorageFileRepresenter.new(storage_file, @storage, current_user:) }, - on_failure: ->(error) { raise_error(error) } + on_failure: ->(error) { raise_service_result_error(error) } ) end end diff --git a/modules/storages/lib/api/v3/storage_files/storage_folders_api.rb b/modules/storages/lib/api/v3/storage_files/storage_folders_api.rb index 9766923a5e0..876eee27b93 100644 --- a/modules/storages/lib/api/v3/storage_files/storage_folders_api.rb +++ b/modules/storages/lib/api/v3/storage_files/storage_folders_api.rb @@ -49,14 +49,10 @@ module API name: params["name"], parent_id: params["parent_id"] ).match( - on_success: lambda { |storage_folder| - API::V3::StorageFiles::StorageFileRepresenter.new( - storage_folder, - @storage, - current_user: - ) + on_success: ->(storage_folder) { + API::V3::StorageFiles::StorageFileRepresenter.new(storage_folder, @storage, current_user:) }, - on_failure: ->(error) { raise_error(error) } + on_failure: ->(error) { raise_service_result_error(error) } ) end end diff --git a/modules/storages/lib/api/v3/storage_files/storage_upload_link_representer.rb b/modules/storages/lib/api/v3/storage_files/storage_upload_link_representer.rb index 02c2eccf17a..c94414c5e61 100644 --- a/modules/storages/lib/api/v3/storage_files/storage_upload_link_representer.rb +++ b/modules/storages/lib/api/v3/storage_files/storage_upload_link_representer.rb @@ -42,8 +42,6 @@ module API::V3::StorageFiles } end - def _type - Storages::UploadLink.name.split("::").last - end + def _type = "UploadLink" end end diff --git a/modules/storages/lib/api/v3/storages/storage_open_api.rb b/modules/storages/lib/api/v3/storages/storage_open_api.rb index 588f09bfe92..d97c5f7d43d 100644 --- a/modules/storages/lib/api/v3/storages/storage_open_api.rb +++ b/modules/storages/lib/api/v3/storages/storage_open_api.rb @@ -35,21 +35,21 @@ class API::V3::Storages::StorageOpenAPI < API::OpenProjectAPI helpers do def auth_strategy - Storages::Peripherals::Registry.resolve("#{@storage}.authentication.user_bound").call(user: current_user, storage: @storage) + Storages::Adapters::Registry.resolve("#{@storage}.authentication.user_bound").call(current_user, @storage) end end resources :open do get do - Storages::Peripherals::Registry + Storages::Adapters::Registry .resolve("#{@storage}.queries.open_storage") .call(storage: @storage, auth_strategy:) - .match( - on_success: ->(url) do + .either( + ->(url) do redirect url, body: "The requested resource can be viewed at #{url}" status 303 end, - on_failure: ->(error) { raise_error(error) } + ->(error) { raise_error(error) } ) end end diff --git a/modules/storages/lib/api/v3/storages/storage_representer.rb b/modules/storages/lib/api/v3/storages/storage_representer.rb index 0de7cde9efc..ab55be4a4fd 100644 --- a/modules/storages/lib/api/v3/storages/storage_representer.rb +++ b/modules/storages/lib/api/v3/storages/storage_representer.rb @@ -263,11 +263,7 @@ module API::V3::Storages end def show_authorize_link? - selector = Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector.new( - user: current_user, storage: represented - ) - - selector.storage_oauth? && + represented.authenticate_via_storage? && represented.oauth_client.present? && authorization_state.in?(%i[not_connected failed_authorization]) end @@ -281,8 +277,7 @@ module API::V3::Storages end def authorization_state - ::Storages::Peripherals::StorageInteraction::Authentication.authorization_state(storage: represented, - user: current_user) + ::Storages::Adapters::Authentication.authorization_state(storage: represented, user: current_user) end end end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_storage_query_spec.rb b/modules/storages/spec/common/storages/adapters/authentication_spec.rb similarity index 51% rename from modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_storage_query_spec.rb rename to modules/storages/spec/common/storages/adapters/authentication_spec.rb index 097fb599156..e345658ee83 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_storage_query_spec.rb +++ b/modules/storages/spec/common/storages/adapters/authentication_spec.rb @@ -31,30 +31,30 @@ require "spec_helper" require_module_spec_helper -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::OpenStorageQuery, :webmock do - using Storages::Peripherals::ServiceResultRefinements +module Storages + module Adapters + RSpec.describe Authentication, :webmock do + let(:user) { create(:user) } - let(:user) { create(:user) } - let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end + let(:noop) { Input::Strategy.build(key: :noop) } + let(:basic_auth) { Input::Strategy.build(key: :basic_auth) } + let(:oauth_client_credentials) { Input::Strategy.build(key: :oauth_client_credentials, use_cache: false) } + let(:oauth_user_token) { Input::Strategy.build(key: :oauth_user_token, user:) } + let(:sso_user_token) { Input::Strategy.build(key: :sso_user_token, user:) } - subject { described_class.new(storage) } + subject(:auth) { described_class } - describe "#call" do - it "responds with correct parameters" do - expect(described_class).to respond_to(:call) + it "instantiates the correct strategy based on the data" do + expect(auth[noop]).to be_a(AuthenticationStrategies::Noop) + expect(auth[basic_auth]).to be_a(AuthenticationStrategies::BasicAuth) + expect(auth[oauth_client_credentials]).to be_a(AuthenticationStrategies::OAuthClientCredentials) + expect(auth[oauth_user_token]).to be_a(AuthenticationStrategies::OAuthUserToken) + expect(auth[sso_user_token]).to be_a(AuthenticationStrategies::SsoUserToken) + end - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy]) - end - - context "with outbound requests successful", vcr: "one_drive/open_storage_query_success" do - it "returns the url for opening the storage" do - call = subject.call(auth_strategy:) - expect(call).to be_success - expect(call.result).to eq("https://finn.sharepoint.com/sites/openprojectfilestoragetests/VCR") + it "returns an error if an unknown strategy is requested" do + broken = Input::Strategy.build(key: :unknown) + expect { auth[broken] }.to raise_error ArgumentError end end end diff --git a/modules/storages/spec/common/storages/adapters/authentication_strategies/basic_auth_spec.rb b/modules/storages/spec/common/storages/adapters/authentication_strategies/basic_auth_spec.rb new file mode 100644 index 00000000000..36e13379e13 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/authentication_strategies/basic_auth_spec.rb @@ -0,0 +1,114 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module AuthenticationStrategies + RSpec.describe BasicAuth, :webmock do + let(:user) { create(:user) } + + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + + let(:request_url) { "#{storage.uri}ocs/v1.php/cloud/user" } + let(:http_options) { { headers: { "OCS-APIRequest" => "true", "Accept" => "application/json" } } } + + let(:strategy_data) { Input::Strategy.build(key: :basic_auth) } + + context "with valid credentials", vcr: "auth/nextcloud/basic_auth" do + before do + storage.username = "admin" + storage.password = "admin" + end + + it "successful response" do + result = Authentication[strategy_data].call(storage:, http_options:) { |http| make_request(http) } + expect(result).to be_success + expect(result.value!).to eq("EXPECTED_RESULT") + end + end + + context "with empty username and password" do + it "must return error" do + result = Authentication[strategy_data].call(storage:, http_options:) { |http| make_request(http) } + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(:missing_credentials) + expect(error.source).to be(described_class) + end + end + + context "with invalid username and/or password", vcr: "auth/nextcloud/basic_auth_password_invalid" do + before do + storage.username = "admin" + storage.password = "YouShallNot(Multi)Pass" + end + + it "must return unauthorized" do + result = Authentication[strategy_data].call(storage:, http_options:) { |http| make_request(http) } + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(:unauthorized) + expect(error.source).to eq("EXECUTING_QUERY") + end + end + + private + + def make_request(http) = handle_response(http.get(request_url)) + + def handle_response(response) + case response + in { status: 200..299 } + Success("EXPECTED_RESULT") + in { status: 401 } + error(:unauthorized) + in { status: 403 } + error(:forbidden) + in { status: 404 } + error(:not_found) + else + error(:error) + end + end + + def error(code) + Failure(Results::Error.new(source: "EXECUTING_QUERY", code:)) + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/specific_bearer_token_spec.rb b/modules/storages/spec/common/storages/adapters/authentication_strategies/bearer_token_spec.rb similarity index 66% rename from modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/specific_bearer_token_spec.rb rename to modules/storages/spec/common/storages/adapters/authentication_strategies/bearer_token_spec.rb index 2086eada9e7..5dbea598a83 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/specific_bearer_token_spec.rb +++ b/modules/storages/spec/common/storages/adapters/authentication_strategies/bearer_token_spec.rb @@ -31,19 +31,25 @@ require "spec_helper" require_module_spec_helper -RSpec.describe Storages::Peripherals::StorageInteraction::AuthenticationStrategies::SpecificBearerToken do - let(:storage) { create(:nextcloud_storage) } - let(:access_token) { "my_access_token" } +module Storages + module Adapters + module AuthenticationStrategies + RSpec.describe BearerToken do + let(:storage) { create(:nextcloud_storage) } + let(:access_token) { "my_access_token" } - it "must yield with passed access token without" do - strategy = described_class.strategy.with_token(access_token) - was_yielded = false + it "must yield with passed access token without" do + strategy = Input::Strategy.build(key: :bearer_token, token: access_token) + was_yielded = false - Storages::Peripherals::StorageInteraction::Authentication[strategy].call(storage:) do |http| - was_yielded = true - expect(http.instance_variable_get(:@options).headers["authorization"]).to eq("Bearer #{access_token}") + Authentication[strategy].call(storage:) do |http| + was_yielded = true + expect(http.instance_variable_get(:@options).headers["authorization"]).to eq("Bearer #{access_token}") + end + + expect(was_yielded).to be_truthy + end + end end - - expect(was_yielded).to be_truthy end end diff --git a/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_client_credentials_spec.rb b/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_client_credentials_spec.rb new file mode 100644 index 00000000000..3ca02aff815 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_client_credentials_spec.rb @@ -0,0 +1,106 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module AuthenticationStrategies + RSpec.describe OAuthClientCredentials, :webmock do + let(:user) { create(:user) } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + + let(:strategy_data) { Input::Strategy.build(key: :oauth_client_credentials, use_cache: false) } + let(:request_url) { "#{storage.uri}v1.0/drives" } + + context "with valid oauth credentials", vcr: "auth/one_drive/client_credentials" do + it "return success" do + result = Authentication[strategy_data].call(storage:, http_options: {}) { |http| make_request(http) } + + expect(result).to be_success + expect(result.value!).to eq("EXPECTED_RESULT") + end + + it "caches the token if use_cache is true" do + strategy_data = Input::Strategy.build(key: :oauth_client_credentials, use_cache: true) + Authentication[strategy_data].call(storage:, http_options: {}) do |http| + make_request(http) + end + + cache_key = described_class::TOKEN_CACHE_KEY % storage.id + expect(Rails.cache.read(cache_key)).not_to be_nil + end + end + + context "with invalid client secret", vcr: "auth/one_drive/client_credentials_invalid_client_secret" do + it "must return unauthorized" do + result = Authentication[strategy_data].call(storage:) { |http| make_request(http) } + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(:unauthorized) + expect(error.source).to eq(described_class) + end + end + + context "with invalid client id", vcr: "auth/one_drive/client_credentials_invalid_client_id" do + it "must return unauthorized" do + result = Authentication[strategy_data].call(storage:) { |http| make_request(http) } + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(:unauthorized) + expect(error.source).to eq(described_class) + end + end + + private + + def make_request(http) = handle_response(http.get(request_url)) + + def handle_response(response) + case response + in { status: 200..299 } + Success("EXPECTED_RESULT") + in { status: 401 } + error(:unauthorized) + else + error(:error) + end + end + + def error(code) + Failure(Results::Error.new(source: "EXECUTING_QUERY", code:)) + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_user_token_spec.rb b/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_user_token_spec.rb new file mode 100644 index 00000000000..cbbb07980af --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_user_token_spec.rb @@ -0,0 +1,147 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module AuthenticationStrategies + RSpec.describe OAuthUserToken, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + let(:request_url) { "#{storage.uri}ocs/v1.php/cloud/user" } + let(:http_options) { { headers: { "OCS-APIRequest" => "true", "Accept" => "application/json" } } } + let(:strategy_data) { Input::Strategy.build(user:, key: :oauth_user_token) } + + subject(:Authentication) { described_class } + + shared_examples_for "successful response" do |refreshed: false| + it "must #{'refresh token and ' if refreshed}return success" do + result = Authentication[strategy_data].call(storage:) { |http| make_request(http) } + expect(result).to be_success + expect(result.value!).to eq("EXPECTED_RESULT") + end + end + + context "with incomplete storage configuration (missing oauth client)" do + let(:storage) { create(:nextcloud_storage) } + + it "must return error" do + result = Authentication[strategy_data].call(storage:) { |http| make_request(http) } + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(:missing_oauth_client) + expect(error.source).to be(described_class) + end + end + + context "with not existent oauth token" do + let(:user_without_token) { create(:user) } + let(:strategy_data) { Input::Strategy.build(user: user_without_token, key: :oauth_user_token) } + + it "must return unauthorized" do + result = Authentication[strategy_data].call(storage:, http_options:) { |http| make_request(http) } + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(:missing_token) + expect(error.source).to be(described_class) + end + end + + context "with invalid oauth refresh token", vcr: "auth/nextcloud/user_token_refresh_token_invalid" do + before { storage } + + it "must return unauthorized" do + result = Authentication[strategy_data].call(storage:, http_options:) { |http| make_request(http) } + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(:unauthorized) + expect(error.source).to be(described_class) + end + + it "logs, retries once, raises exception if race condition happens" do + token = OAuthClientToken.last + strategy = Authentication[strategy_data] + + allow(Rails.logger).to receive(:error) + allow(strategy).to receive(:current_token).and_return(Success(token)) + allow(token).to receive(:destroy).and_raise(ActiveRecord::StaleObjectError).twice + + expect do + strategy.call(storage:, http_options:) { |http| make_request(http) } + end.to raise_error(ActiveRecord::StaleObjectError) + + expect(Rails.logger).to have_received(:error).with(/User ##{user.id} #{user.name}/).once + end + end + + context "with invalid oauth access token", vcr: "auth/nextcloud/user_token_access_token_invalid" do + it_behaves_like "successful response", refreshed: true + end + + context "with valid access token", vcr: "auth/one_drive/user_token" do + let(:request_url) { "#{storage.uri}v1.0/me" } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + + it_behaves_like "successful response" + end + + private + + def make_request(http) = handle_response(http.get(request_url)) + + def handle_response(response) + case response + in { status: 200..299 } + Success("EXPECTED_RESULT") + in { status: 401 } + error(:unauthorized) + in { status: 403 } + error(:forbidden) + in { status: 404 } + error(:not_found) + else + error(:error) + end + end + + def error(code) + Failure(Results::Error.new(source: "EXECUTING_QUERY", code:)) + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/authentication_strategies/sso_user_token_spec.rb b/modules/storages/spec/common/storages/adapters/authentication_strategies/sso_user_token_spec.rb new file mode 100644 index 00000000000..fa38bb6d8b4 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/authentication_strategies/sso_user_token_spec.rb @@ -0,0 +1,83 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module AuthenticationStrategies + RSpec.describe SsoUserToken do + let(:storage) { create(:nextcloud_storage) } + + subject(:strategy) { described_class.new(create(:user)) } + + before do + service = instance_double(OpenIDConnect::UserTokens::FetchService) + allow(OpenIDConnect::UserTokens::FetchService).to receive(:new).and_return(service) + allow(service).to receive(:access_token_for).with(audience: storage.audience).and_return(access_token_result) + end + + context "if access token can be fetched successfully" do + let(:token) { "my_access_token" } + let(:access_token_result) { Success(token) } + + it "must yield with access token" do + was_yielded = false + + strategy.call(storage:) do |http| + was_yielded = true + expect(http.instance_variable_get(:@options).headers["authorization"]).to eq("Bearer #{token}") + end + + expect(was_yielded).to be_truthy + end + end + + context "if fetching access token fails" do + let(:error) { Results::Error.new(code: :error, source: self) } + let(:access_token_result) { Failure(error) } + + it "must not yield and return failure" do + was_yielded = false + result = strategy.call(storage:) { was_yielded = true } + + expect(was_yielded).to be_falsy + expect(result).to be_failure + + failure = result.failure + expect(failure.code).to eq(:unauthorized) + expect(failure).to be_a(Results::Error) + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/base_connection_validator_spec.rb b/modules/storages/spec/common/storages/adapters/connection_validators/base_connection_validator_spec.rb similarity index 76% rename from modules/storages/spec/common/storages/peripherals/connection_validators/base_connection_validator_spec.rb rename to modules/storages/spec/common/storages/adapters/connection_validators/base_connection_validator_spec.rb index 943dd3d4c08..74064e03536 100644 --- a/modules/storages/spec/common/storages/peripherals/connection_validators/base_connection_validator_spec.rb +++ b/modules/storages/spec/common/storages/adapters/connection_validators/base_connection_validator_spec.rb @@ -32,7 +32,7 @@ require "spec_helper" require_module_spec_helper module Storages - module Peripherals + module Adapters module ConnectionValidators class TestValidator < BaseConnectionValidator def self.reset_groups! @@ -52,7 +52,7 @@ module Storages end it "only runs a verification if the precondition evaluates as truthy" do - test_group = class_spy(Nextcloud::StorageConfigurationValidator) + test_group = class_spy(Providers::Nextcloud::Validators::StorageConfigurationValidator) TestValidator.register_group test_group, precondition: ->(_, _) { false } result = validator.call @@ -61,17 +61,19 @@ module Storages end it "aggregates all the results from the tests", vcr: "nextcloud/capabilities_success" do - TestValidator.register_group Nextcloud::StorageConfigurationValidator - TestValidator.register_group Nextcloud::AuthenticationValidator, + TestValidator.register_group Providers::Nextcloud::Validators::StorageConfigurationValidator + TestValidator.register_group Providers::Nextcloud::Validators::AuthenticationValidator, precondition: ->(_, result) do - result.group(Nextcloud::StorageConfigurationValidator.key).non_failure? + result.group( + Providers::Nextcloud::Validators::StorageConfigurationValidator.key + ).non_failure? end results = TestValidator.new(create(:nextcloud_storage_with_local_connection)).call expect(results).to be_warning - expect(results.group(Nextcloud::StorageConfigurationValidator.key)).to be_success - expect(results.group(Nextcloud::AuthenticationValidator.key)).to be_warning + expect(results.group(Providers::Nextcloud::Validators::StorageConfigurationValidator.key)).to be_success + expect(results.group(Providers::Nextcloud::Validators::AuthenticationValidator.key)).to be_warning end end end diff --git a/modules/storages/spec/common/storages/adapters/input/create_folder_spec.rb b/modules/storages/spec/common/storages/adapters/input/create_folder_spec.rb new file mode 100644 index 00000000000..53b07023803 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/input/create_folder_spec.rb @@ -0,0 +1,65 @@ +# 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 Adapters + module Input + RSpec.describe CreateFolder do + subject(:input) { described_class } + + describe ".new" do + it "discourages direct instantiation" do + expect { described_class.new(file_id: "file_id", parent_location: "/") } + .to raise_error(NoMethodError, /private method 'new'/) + end + end + + describe ".build" do + it "creates a success result for valid input data" do + expect(input.build(parent_location: "/", folder_name: "DeathStar")).to be_success + end + + it "coerces the parent folder into a ParentFolder object" do + result = input.build(parent_location: "/", folder_name: "DeathStar").value! + + expect(result.parent_location).to be_a(Peripherals::ParentFolder) + end + + it "creates a failure result for invalid input data" do + expect(input.build(parent_location: "/", folder_name: 1)).to be_failure + expect(input.build(parent_location: "/", folder_name: "")).to be_failure + expect(input.build(parent_location: 1, folder_name: "DeathStar")).to be_failure + expect(input.build(parent_location: "", folder_name: "DeathStar")).to be_failure + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/open_file_link_query.rb b/modules/storages/spec/common/storages/adapters/input/file_info_spec.rb similarity index 65% rename from modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/open_file_link_query.rb rename to modules/storages/spec/common/storages/adapters/input/file_info_spec.rb index 9e5225a07bf..e47c8d24dc0 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/open_file_link_query.rb +++ b/modules/storages/spec/common/storages/adapters/input/file_info_spec.rb @@ -29,26 +29,27 @@ #++ module Storages - module Peripherals - module StorageInteraction - module Nextcloud - class OpenFileLinkQuery - def self.call(storage:, auth_strategy:, file_id:, open_location: false) - new(storage).call(auth_strategy:, file_id:, open_location:) + module Adapters + module Input + RSpec.describe FileInfo do + subject(:input) { described_class } + + describe ".new" do + it "discourages direct instantiation" do + expect { described_class.new(file_id: "file_id") } + .to raise_error(NoMethodError, /private method 'new'/) + end + end + + describe ".build" do + it "creates a success result for valid input data" do + expect(input.build(file_id: "1234")).to be_success end - def initialize(storage) - @storage = storage + it "creates a failure result for invalid input data" do + expect(input.build(file_id: 1)).to be_failure + expect(input.build(file_id: "")).to be_failure end - - # rubocop:disable Lint/UnusedMethodArgument - def call(auth_strategy:, file_id:, open_location: false) - location_flag = open_location ? 0 : 1 - url = UrlBuilder.url(@storage.uri, "index.php/f/#{file_id}") + "?openfile=#{location_flag}" - ServiceResult.success(result: url) - end - - # rubocop:enable Lint/UnusedMethodArgument end end end diff --git a/modules/storages/spec/common/storages/adapters/input/file_path_to_id_map_spec.rb b/modules/storages/spec/common/storages/adapters/input/file_path_to_id_map_spec.rb new file mode 100644 index 00000000000..eaaebea887c --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/input/file_path_to_id_map_spec.rb @@ -0,0 +1,71 @@ +# 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 Adapters + module Input + RSpec.describe FilePathToIdMap do + subject(:input) { described_class } + + describe ".new" do + it "discourages direct instantiation" do + expect { described_class.new(folder: "file_id", depth: 1) } + .to raise_error(NoMethodError, /private method 'new'/) + end + end + + describe ".build" do + it "creates a success result for valid input data" do + expect(input.build(folder: "/")).to be_success + end + + it "coerces the parent folder into a ParentFolder object" do + result = input.build(folder: "/", depth: 1).value! + + expect(result.folder).to be_a(Peripherals::ParentFolder) + end + + it "defaults to Float::INFINITY for depth" do + result = input.build(folder: "/").value! + + expect(result.depth).to eq(Float::INFINITY) + end + + it "creates a failure result for invalid input data" do + expect(input.build(folder: "/", depth: 1.5)).to be_failure + expect(input.build(folder: "/", depth: -1)).to be_failure + expect(input.build(folder: 1, depth: 1)).to be_failure + expect(input.build(folder: "", depth: 1)).to be_failure + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/strategy.rb b/modules/storages/spec/common/storages/adapters/input/files_spec.rb similarity index 60% rename from modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/strategy.rb rename to modules/storages/spec/common/storages/adapters/input/files_spec.rb index 0e2680a4758..30ba6d8842e 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/strategy.rb +++ b/modules/storages/spec/common/storages/adapters/input/files_spec.rb @@ -29,40 +29,32 @@ #++ module Storages - module Peripherals - module StorageInteraction - module AuthenticationStrategies - class Strategy - attr_reader :key, :user, :use_cache, :token + module Adapters + module Input + RSpec.describe Files do + subject(:input) { described_class } - def initialize(key) - @key = key - # per default authorization strategies are using the cache - # to reduce the number authentication requests - @use_cache = true + describe ".new" do + it "discourages direct instantiation" do + expect { described_class.new(file_id: "file_id", user_permissions: []) } + .to raise_error(NoMethodError, /private method 'new'/) + end + end + + describe ".build" do + it "creates a success result for valid input data" do + expect(input.build(folder: "DeathStar")).to be_success end - def with_user(user) - @user = user - self + it "coerces the parent folder into a ParentFolder object" do + result = input.build(folder: "DeathStar").value! + + expect(result.folder).to be_a(Peripherals::ParentFolder) end - def with_cache(use_cache) - @use_cache = use_cache - self - end - - def with_token(token) - @token = token - self - end - - def ==(other) - @key == other.key && @use_cache == other.use_cache && @user == other.user && @token == other.token - end - - def hash - [@key, @use_cache, @user, @token].hash + it "creates a failure result for invalid input data" do + expect(input.build(folder: 1)).to be_failure + expect(input.build(folder: "")).to be_failure end end end diff --git a/modules/storages/spec/common/storages/adapters/input/rename_file_spec.rb b/modules/storages/spec/common/storages/adapters/input/rename_file_spec.rb new file mode 100644 index 00000000000..4851a13d564 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/input/rename_file_spec.rb @@ -0,0 +1,65 @@ +# 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 Adapters + module Input + RSpec.describe RenameFile do + subject(:input) { described_class } + + describe ".new" do + it "discourages direct instantiation" do + expect { described_class.new(location: "file_id", new_name: "Bob") } + .to raise_error(NoMethodError, /private method 'new'/) + end + end + + describe ".build" do + it "creates a success result for valid input data" do + expect(input.build(location: "/", new_name: "DeathStar")).to be_success + end + + it "coerces the parent folder into a ParentFolder object" do + result = input.build(location: "/", new_name: "DeathStar").value! + + expect(result.location).to be_a(Peripherals::ParentFolder) + end + + it "creates a failure result for invalid input data" do + expect(input.build(location: "/", new_name: 1)).to be_failure + expect(input.build(location: "/", new_name: "")).to be_failure + expect(input.build(location: 1, new_name: "DeathStar")).to be_failure + expect(input.build(location: "", new_name: "DeathStar")).to be_failure + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/input/set_permissions_spec.rb b/modules/storages/spec/common/storages/adapters/input/set_permissions_spec.rb new file mode 100644 index 00000000000..e77c5ce099f --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/input/set_permissions_spec.rb @@ -0,0 +1,83 @@ +# 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" + +module Storages + module Adapters + module Input + RSpec.describe SetPermissions do + describe ".new" do + it "discourages direct instantiation" do + expect { described_class.new(file_id: "file_id", user_permissions: []) } + .to raise_error(NoMethodError, /private method 'new'/) + end + end + + describe ".build" do + it "creates a success result for valid input data" do + expect(described_class.build(file_id: "1337", user_permissions: [])).to be_success + expect(described_class.build(file_id: "1337", + user_permissions: [{ user_id: "dart_vader", permissions: [] }])).to be_success + expect(described_class + .build( + file_id: "1337", + user_permissions: [ + { user_id: "dart_vader", permissions: %i[read_files write_files delete_files] }, + { group_id: "stormtroopers", permissions: [:read_files] } + ] + )).to be_success + end + + it "creates a failure result for invalid input data" do + expect(described_class.build(file_id: nil, user_permissions: [])).to be_failure + expect(described_class.build(file_id: "", user_permissions: [])).to be_failure + expect(described_class.build(file_id: "1337", user_permissions: {})).to be_failure + + expect(described_class.build(file_id: "1337", user_permissions: [:read_files])).to be_failure + expect(described_class.build(file_id: "1337", user_permissions: [{ user: "rey", permissions: [] }])).to be_failure + expect(described_class.build(file_id: "1337", user_permissions: [{ user_id: "rey" }])).to be_failure + expect(described_class.build(file_id: "1337", + user_permissions: [{ user_id: "rey", permissions: [:read] }])).to be_failure + expect(described_class.build(file_id: "1337", + user_permissions: [{ user_id: "rey", permissions: {} }])).to be_failure + + expect(described_class.build(file_id: "1337", user_permissions: [{ permissions: [:read_files] }])).to be_failure + expect(described_class + .build( + file_id: "1337", + user_permissions: [{ user_id: "rey", group_id: "jedi", permissions: [:read_files] }] + )).to be_failure + end + end + end + end + end +end diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/set_permissions_contract.rb b/modules/storages/spec/common/storages/adapters/input/upload_link_spec.rb similarity index 58% rename from modules/storages/app/common/storages/peripherals/storage_interaction/inputs/set_permissions_contract.rb rename to modules/storages/spec/common/storages/adapters/input/upload_link_spec.rb index e63f95b7fb7..267ed0531e5 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/inputs/set_permissions_contract.rb +++ b/modules/storages/spec/common/storages/adapters/input/upload_link_spec.rb @@ -29,25 +29,28 @@ #++ module Storages - module Peripherals - module StorageInteraction - module Inputs - class SetPermissionsContract < Dry::Validation::Contract - params do - required(:file_id).filled(:string) - required(:user_permissions).array(:hash) do - optional(:user_id).filled(:string) - optional(:group_id).filled(:string) - required(:permissions) - .array(:symbol, included_in?: OpenProject::Storages::Engine.external_file_permissions) - end + module Adapters + module Input + RSpec.describe UploadLink do + subject(:input) { described_class } + + describe ".new" do + it "discourages direct instantiation" do + expect { described_class.new(folder_id: "file_id", file_name: "name") } + .to raise_error(NoMethodError, /private method 'new'/) + end + end + + describe ".build" do + it "creates a success result for valid input data" do + expect(input.build(folder_id: "ABDCE", file_name: "DeathStar")).to be_success end - rule(:user_permissions).each do - both = value.key?(:user_id) && value.key?(:group_id) - none = !value.key?(:user_id) && !value.key?(:group_id) - - key.failure("must have either user_id or group_id") if both || none + it "creates a failure result for invalid input data" do + expect(input.build(folder_id: "/", file_name: 1)).to be_failure + expect(input.build(folder_id: "/", file_name: "")).to be_failure + expect(input.build(folder_id: 1, file_name: "DeathStar")).to be_failure + expect(input.build(folder_id: "", file_name: "DeathStar")).to be_failure end end end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/network_error_spec.rb b/modules/storages/spec/common/storages/adapters/network_error_spec.rb similarity index 74% rename from modules/storages/spec/common/storages/peripherals/storage_interaction/network_error_spec.rb rename to modules/storages/spec/common/storages/adapters/network_error_spec.rb index 9618cb385f5..7b4273b1a9a 100644 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/network_error_spec.rb +++ b/modules/storages/spec/common/storages/adapters/network_error_spec.rb @@ -33,28 +33,25 @@ require_module_spec_helper # rubocop:disable RSpec/DescribeClass RSpec.describe "network errors for storage interaction", :webmock do - using Storages::Peripherals::ServiceResultRefinements - let(:user) { create(:user) } let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - let(:fields) { Storages::Peripherals::StorageInteraction::OneDrive::FilesQuery::FIELDS } + let(:fields) { Storages::Adapters::Providers::OneDrive::Queries::FilesQuery::FIELDS } let(:request_url) { "https://graph.microsoft.com/v1.0/drives/#{storage.drive_id}/root/children#{fields}" } - let(:folder) { Storages::Peripherals::ParentFolder.new("/") } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end + let(:input_data) { Storages::Adapters::Input::Files.build(folder: "/").value! } + let(:auth_strategy) { Storages::Adapters::Input::Strategy.build(key: :oauth_user_token, user:) } context "if a timeout happens" do it "must return an error with wrapped network error response" do # Test network error handling specifically with the files query. # Other queries and commands should implement the network error handling in the same way. stub_request(:get, request_url).to_timeout - result = Storages::Peripherals::StorageInteraction::OneDrive::FilesQuery.call(storage:, auth_strategy:, folder:) + result = Storages::Adapters::Providers::OneDrive::Queries::FilesQuery.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - expect(result.result).to eq(:error) - expect(result.error_source).to be(Storages::Peripherals::StorageInteraction::OneDrive::FilesQuery) - expect(result.error_payload).to be_a(HTTPX::ErrorResponse) + failure = result.failure + expect(failure.code).to eq(:error) + expect(failure.source).to be(Storages::Adapters::Providers::OneDrive::Queries::FilesQuery) + expect(failure.payload).to be_a(HTTPX::ErrorResponse) end end end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/add_user_to_group_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/add_user_to_group_command_spec.rb new file mode 100644 index 00000000000..8aad4d49d49 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/add_user_to_group_command_spec.rb @@ -0,0 +1,143 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Commands + RSpec.describe AddUserToGroupCommand, :webmock do + let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed, username: "vcr") } + let(:auth_strategy) { Registry.resolve("nextcloud.authentication.userless").call } + let(:input_data) { Input::AddUserToGroup.build(group:, user:).value! } + + describe "basic command setup" do + it "is registered as commands.add_user_to_group" do + expect(Registry.resolve("#{storage}.commands.add_user_to_group")).to eq(described_class) + end + + it "responds to #call with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy], + %i[keyreq input_data]) + end + end + + shared_examples_for "failing request" do |error_code:| + it "returns a failure" do + result = described_class.call(storage:, auth_strategy:, input_data:) + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(error_code) + expect(error.source).to eq(described_class) + end + end + + context "if group exists", vcr: "nextcloud/add_user_to_group_success" do + let(:user) { "m.jade@death.star" } + let(:group) { "Sith Assassins" } + + before do + create_group(auth, storage, group) + end + + after do + remove_group(auth, storage, group) + end + + it "returns a success" do + members = group_members(group) + expect(members).not_to include(user) + + result = described_class.call(storage:, auth_strategy:, input_data:) + expect(result).to be_success + + members = group_members(group) + expect(members).to include(user) + end + end + + context "if target group does not exist", vcr: "nextcloud/add_user_to_group_not_existing_group" do + let(:user) { "m.jade@death.star" } + let(:group) { "Sith Assassins" } + + it_behaves_like "failing request", error_code: :group_does_not_exist + end + + context "if user does not exist", vcr: "nextcloud/add_user_to_group_not_existing_user" do + let(:user) { "this is not the user you are looking for" } + let(:group) { "Sith Assassins" } + + before do + create_group(auth, storage, group) + end + + after do + remove_group(auth, storage, group) + end + + it_behaves_like "failing request", error_code: :user_does_not_exist + end + + private + + def auth = Authentication[auth_strategy] + + def group_members(group) + Input::GroupUsers.build(group:).bind do |input_data| + Queries::GroupUsersQuery.call(storage:, auth_strategy:, input_data:).value! + end + end + + def create_group(authentication, storage, group) + authentication.call(storage:, http_options: { headers: { "OCS-APIRequest" => "true" } }) do |http| + http.post(UrlBuilder.url(storage.uri, "ocs/v1.php/cloud/groups"), + form: { "groupid" => group }) + end + end + + def remove_group(authentication, storage, group) + authentication.call(storage:, http_options: { headers: { "OCS-APIRequest" => "true" } }) do |http| + http.delete(UrlBuilder.url(storage.uri, "/ocs/v1.php/cloud/groups", group)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/copy_template_folder_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/copy_template_folder_command_spec.rb new file mode 100644 index 00000000000..71225cace4a --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/copy_template_folder_command_spec.rb @@ -0,0 +1,158 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Commands + RSpec.describe CopyTemplateFolderCommand, :webmock do + let(:user) { create(:user) } + let(:url) { "https://example.com" } + let(:origin_user_id) { "OpenProject" } + let(:storage) do + build(:nextcloud_storage, :as_automatically_managed, host: url, password: "OpenProjectSecurePassword") + end + + let(:source) { "/source-of-fun" } + let(:destination) { "/boring-destination" } + let(:source_url) { "#{url}/remote.php/dav/files/#{CGI.escape(origin_user_id)}#{source}" } + let(:destination_url) { "#{url}/remote.php/dav/files/#{CGI.escape(origin_user_id)}#{destination}" } + let(:auth_strategy) { Registry["nextcloud.authentication.userless"].call } + let(:input_data) { Input::CopyTemplateFolder.build(source:, destination:).value! } + + subject(:command) { described_class.new(storage) } + + describe "#call" do + before { stub_request(:head, destination_url).to_return(status: 404) } + + # describe "parameter validation" do + # it "source cannot be blank" do + # result = command.call(auth_strategy:, source: "", destination: "/destination") + # + # expect(result).to be_failure + # expect(result.errors.log_message).to eq("Source and destination paths must be present.") + # end + # + # it "destination cannot blank" do + # result = command.call(auth_strategy:, source: "/source", destination: "") + # + # expect(result).to be_failure + # expect(result.errors.log_message).to eq("Source and destination paths must be present.") + # end + # end + + describe "remote server overwrite protection" do + it "destination must not exist on the remote server" do + stub_request(:head, destination_url).to_return(status: 200) + result = command.call(auth_strategy:, input_data:) + + expect(result).to be_failure + expect(result.failure.code).to eq(:conflict) + end + end + + context "when the folder is copied successfully" do + let(:successful_propfind) do + <<~XML + + + + /remote.php/dav/files/#{CGI.escape(origin_user_id)}#{destination} + + + 349 + + HTTP/1.1 200 OK + + + + /remote.php/dav/files/#{CGI.escape(origin_user_id)}#{destination}/Dinge/ + + + 783 + + HTTP/1.1 200 OK + + + + XML + end + + before do + stub_request(:copy, source_url).to_return(status: 201) + stub_request(:propfind, destination_url).to_return(status: 200, body: successful_propfind) + end + + it "must be successful" do + result = command.call(auth_strategy:, input_data:) + + expect(result).to be_success + expect(result.value!.id).to eq("349") + end + end + + describe "error handling" do + before do + body = <<~XML + + + Sabre\\DAV\\Exception\\Conflict + The destination node is not found + + XML + stub_request(:copy, source_url).to_return(status: 409, body:, headers: { "Content-Type" => "application/xml" }) + end + + it "returns a :conflict failure if the copy fails" do + result = command.call(auth_strategy:, input_data:) + + expect(result).to be_failure + + failure = result.failure + expect(failure.code).to eq(:conflict) + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/create_folder_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/create_folder_command_spec.rb new file mode 100644 index 00000000000..a94d6c2d906 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/create_folder_command_spec.rb @@ -0,0 +1,116 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Commands + RSpec.describe CreateFolderCommand, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + + let(:auth_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + let(:input_data) { Input::CreateFolder.build(folder_name:, parent_location:).value! } + + it_behaves_like "adapter create_folder_command: basic command setup" + + context "when creating a folder in the root", vcr: "nextcloud/create_folder_root" do + let(:folder_name) { "Földer CreatedBy Çommand" } + let(:parent_location) { "/" } + let(:path) { "/#{folder_name}" } + + it_behaves_like "adapter create_folder_command: successful folder creation" + end + + context "when creating a folder in a parent folder", vcr: "nextcloud/create_folder_parent" do + let(:folder_name) { "Földer CreatedBy Çommand" } + let(:parent_location) { "/Folder" } + let(:path) { "/Folder/#{folder_name}" } + + it_behaves_like "adapter create_folder_command: successful folder creation" + end + + context "when creating a folder in a non-existing parent folder", vcr: "nextcloud/create_folder_parent_not_found" do + let(:folder_name) { "New Folder" } + let(:parent_location) { "/DeathStar3" } + let(:error_source) { described_class } + + it_behaves_like "adapter create_folder_command: parent not found" + end + + context "when folder already exists", vcr: "nextcloud/create_folder_already_exists" do + let(:folder_name) { "Folder" } + let(:parent_location) { "/" } + let(:error_source) { described_class } + + it_behaves_like "adapter create_folder_command: folder already exists" + end + + # For the VCR tests in this block it's necessary to + # create a user in NextCloud with the account name `member@example1` + # and use it's oauth access & refresh tokens on .env.test.local + describe "user with custom origin name" do + let(:storage) do + create( + :nextcloud_storage_with_local_connection, + :as_not_automatically_managed, + origin_user_id: "member@example1", + oauth_client_token_user: user + ) + end + + context "when creating a folder as a non admin user", vcr: "nextcloud/create_folder_member" do + let(:folder_name) { "Földer CreatedBy Çommand" } + let(:parent_location) { "/" } + let(:path) { "/#{folder_name}" } + + it_behaves_like "adapter create_folder_command: successful folder creation" + end + end + + private + + def delete_created_folder(folder) + Input::DeleteFolder.build(location: folder.location).bind do |input_data| + Registry.resolve("nextcloud.commands.delete_folder").call(storage:, auth_strategy:, input_data:) + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/delete_folder_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/delete_folder_command_spec.rb new file mode 100644 index 00000000000..43edb9cd7f4 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/delete_folder_command_spec.rb @@ -0,0 +1,89 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Commands + RSpec.describe DeleteFolderCommand, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + let(:auth_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + + it "is registered as commands.nextcloud.delete_folder" do + expect(Registry.resolve("nextcloud.commands.delete_folder")).to eq(described_class) + end + + describe "#call" do + it "responds with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy], + %i[keyreq input_data]) + end + + it "deletes a folder", vcr: "nextcloud/delete_folder" do + Input::CreateFolder.build(folder_name: "To Be Deleted Soon", parent_location: "/").bind do |input_data| + Registry.resolve("nextcloud.commands.create_folder").call(storage:, auth_strategy:, input_data:) + end + + Input::DeleteFolder.build(location: "/To Be Deleted Soon").bind do |input_data| + expect(described_class.call(storage:, auth_strategy:, input_data:)).to be_success + end + end + + context "if folder does not exist" do + it "returns a failure", vcr: "nextcloud/delete_folder_not_found" do + result = Input::DeleteFolder.build(location: "/IDoNotExist").bind do |input_data| + described_class.call(storage:, auth_strategy:, input_data:) + end + + expect(result).to be_failure + error = result.failure + + expect(error.source).to be(described_class) + expect(error.code).to eq(:not_found) + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/remove_user_from_group_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/remove_user_from_group_command_spec.rb new file mode 100644 index 00000000000..28039b74c90 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/remove_user_from_group_command_spec.rb @@ -0,0 +1,173 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Commands + RSpec.describe RemoveUserFromGroupCommand, :webmock do + let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed, username: "vcr") } + let(:auth_strategy) { Registry.resolve("nextcloud.authentication.userless").call } + let(:input_data) { Input::RemoveUserFromGroup.build(group:, user:).value! } + + describe "basic command setup" do + it "is registered as commands.remove_user_from_group" do + expect(Registry.resolve("#{storage}.commands.remove_user_from_group")).to eq(described_class) + end + + it "responds to #call with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy], + %i[keyreq input_data]) + end + end + + shared_examples_for "failing request" do |error_code:| + it "returns a failure" do + result = described_class.call(storage:, auth_strategy:, input_data:) + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(error_code) + expect(error.source).to eq(described_class) + end + end + + context "if user exists in target group", vcr: "nextcloud/remove_user_from_group_success" do + let(:user) { "m.jade@death.star" } + let(:group) { "Sith Assassins" } + + before do + create_group(auth, storage, group) + add_user_to_group(user, group) + end + + after do + remove_group(auth, storage, group) + end + + it "returns a success" do + members = group_members(group) + expect(members).to include(user) + + result = described_class.call(storage:, auth_strategy:, input_data:) + expect(result).to be_success + + members = group_members(group) + expect(members).not_to include(user) + end + end + + context "if user does not exist in target group", vcr: "nextcloud/remove_user_from_group_no_user" do + let(:user) { "m.jade@death.star" } + let(:group) { "Sith Assassins" } + + before do + create_group(auth, storage, group) + end + + after do + remove_group(auth, storage, group) + end + + it "returns a success" do + members = group_members(group) + expect(members).not_to include(user) + + result = described_class.call(storage:, auth_strategy:, input_data:) + expect(result).to be_success + + members = group_members(group) + expect(members).not_to include(user) + end + end + + context "if target group does not exist", vcr: "nextcloud/remove_user_from_group_not_existing_group" do + let(:user) { "m.jade@death.star" } + let(:group) { "Sith Assassins" } + + it_behaves_like "failing request", error_code: :group_does_not_exist + end + + context "if user does not exist", vcr: "nextcloud/remove_user_from_group_not_existing_user" do + let(:user) { "this is not the user you are looking for" } + let(:group) { "Sith Assassins" } + + before do + create_group(auth, storage, group) + end + + after do + remove_group(auth, storage, group) + end + + it_behaves_like "failing request", error_code: :user_does_not_exist + end + + private + + def auth = Authentication[auth_strategy] + + def add_user_to_group(user, group) + Input::AddUserToGroup.build(user:, group:).bind do |input_data| + AddUserToGroupCommand.call(storage:, auth_strategy:, input_data:) + end + end + + def group_members(group) + Input::GroupUsers.build(group:).bind do |input_data| + Queries::GroupUsersQuery.call(storage:, auth_strategy:, input_data:).value! + end + end + + def create_group(authentication, storage, group) + authentication.call(storage:, http_options: { headers: { "OCS-APIRequest" => "true" } }) do |http| + http.post(UrlBuilder.url(storage.uri, "ocs/v1.php/cloud/groups"), form: { "groupid" => group }) + end + end + + def remove_group(authentication, storage, group) + authentication.call(storage:, http_options: { headers: { "OCS-APIRequest" => "true" } }) do |http| + http.delete(UrlBuilder.url(storage.uri, "/ocs/v1.php/cloud/groups", group)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/rename_file_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/rename_file_command_spec.rb new file mode 100644 index 00000000000..4cdde1c7ce0 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/rename_file_command_spec.rb @@ -0,0 +1,76 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Commands + RSpec.describe RenameFileCommand, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + + let(:auth_strategy) { Registry.resolve("nextcloud.authentication.user_bound").call(user, storage) } + let(:input_data) { Input::RenameFile.build(location: file_id, new_name: name).value! } + + it_behaves_like "adapter rename_file_command: basic command setup" + + context "when renaming a folder", vcr: "nextcloud/rename_file_success" do + let(:file_id) { "169" } + let(:name) { "I am the senat" } + + it_behaves_like "adapter rename_file_command: successful file renaming" + end + + context "when renaming a file inside a subdirectory", vcr: "nextcloud/rename_file_with_location_success" do + let(:file_id) { "167" } + let(:name) { "I❤️you death star.md" } + + it_behaves_like "adapter rename_file_command: successful file renaming" + end + + context "when trying to rename a not existent file", vcr: "nextcloud/rename_file_not_found" do + let(:file_id) { "sith_have_yellow_light_sabers" } + let(:name) { "this_will_not_happen.txt" } + let(:error_source) { Queries::FileInfoQuery } + + it_behaves_like "adapter rename_file_command: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/set_permissions_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/set_permissions_command_spec.rb new file mode 100644 index 00000000000..b5c29c91d9e --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/commands/set_permissions_command_spec.rb @@ -0,0 +1,157 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Commands + RSpec.describe SetPermissionsCommand, :webmock do + let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed, username: "vcr") } + let(:auth_strategy) { Registry.resolve("nextcloud.authentication.userless").call } + + let(:test_folder) do + Input::CreateFolder.build(folder_name: "Permission Test Folder", parent_location: "/VCR").bind do |input_data| + Registry.resolve("nextcloud.commands.create_folder").call(storage:, auth_strategy:, input_data:).value! + end + end + + it_behaves_like "adapter set_permissions_command: basic command setup" + + context "if folder does not exists", vcr: "nextcloud/set_permissions_not_found_folder" do + let(:error_source) { Queries::FileInfoQuery } + let(:input_data) { permission_input_data("1337", []) } + + it_behaves_like "adapter set_permissions_command: not found" + end + + context "if no permissions exist", vcr: "nextcloud/set_permissions_new" do + let(:user_permissions) do + [ + { user_id: "m.jade@death.star", permissions: %i[read_files write_files] }, + { user_id: "admin", permissions: %i[read_files write_files create_files delete_files] } + ] + end + + it_behaves_like "adapter set_permissions_command: creates new permissions" + end + + context "if a permission set already exist", vcr: "nextcloud/set_permissions_replacing_permissions" do + let(:previous_permissions) do + [{ user_id: "admin", permissions: %i[read_files write_files create_files delete_files] }] + end + let(:replacing_permissions) do + [{ user_id: "m.jade@death.star", permissions: %i[read_files write_files] }] + end + + it_behaves_like "adapter set_permissions_command: replaces already set permissions" + end + + context "if a user does not exist", + skip: "When setting permissions for a user that does not exists, nextcloud's response doesn't contain the " \ + "needed information. We need to work around this by maybe having a separate request fetching ACLs " \ + "after setting them.", + vcr: "nextcloud/set_permissions_invalid_user_id" do + let(:user_permissions) do + [{ user_id: "luke_the_sky", permissions: %i[read_files write_files create_files delete_files share_files] }] + end + + it_behaves_like "adapter set_permissions_command: unknown remote identity" + end + + private + + def permission_input_data(file_id, user_permissions) + Input::SetPermissions.build(file_id:, user_permissions:).value! + end + + def current_remote_permissions + Authentication[auth_strategy].call(storage:) do |http| + request_url = UrlBuilder.url(storage.uri, + "remote.php/dav/files", + storage.username, + test_folder.location) + response = http.request(:propfind, request_url, xml: permission_request_body) + parse_acl_xml response.body.to_s + end + end + + def permission_request_body + Nokogiri::XML::Builder.new do |xml| + xml["d"].propfind( + "xmlns:d" => "DAV:", + "xmlns:nc" => "http://nextcloud.org/ns" + ) do + xml["d"].prop do + xml["nc"].send(:"acl-list") + end + end + end.to_xml + end + + def parse_acl_xml(xml) + found_code = "d:status[text() = 'HTTP/1.1 200 OK']" + not_found_code = "d:status[text() = 'HTTP/1.1 404 Not Found']" + happy_path = "/d:multistatus/d:response/d:propstat[#{found_code}]/d:prop/nc:acl-list" + not_found_path = "/d:multistatus/d:response/d:propstat[#{not_found_code}]/d:prop" + + if Nokogiri::XML(xml).xpath(not_found_path).children.map(&:name).include?("acl-list") + [] + else + Nokogiri::XML(xml).xpath(happy_path).children.map do |acl| + acl.children.each_with_object({ user_id: "", permissions: [] }) do |entry, agg| + agg[:user_id] = entry.text if entry.name == "acl-mapping-id" + agg[:permissions] = translate_mask_to_permissions(entry.text.to_i) if entry.name == "acl-permissions" + end + end + end + end + + def translate_mask_to_permissions(number) + described_class::PERMISSIONS_MAP.each_with_object([]) do |(permission, mask), list| + list << permission if number & mask == mask + end + end + + # TODO: Delete folder for nextcloud still works on a location, not a file id. + def clean_up(_) + Input::DeleteFolder.build(location: test_folder.location).bind do |input_data| + Registry.resolve("nextcloud.commands.delete_folder").call(storage:, auth_strategy:, input_data:) + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/contracts/storage_contract_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/contracts/storage_contract_spec.rb new file mode 100644 index 00000000000..c8fa2c6a15a --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/contracts/storage_contract_spec.rb @@ -0,0 +1,236 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Contracts + RSpec.describe StorageContract, :storage_server_helpers, :webmock do + let(:current_user) { create(:admin) } + let(:storage) { build(:nextcloud_storage) } + let(:mocked_host) { storage.host } + + let!(:capabilities_request) { mock_server_capabilities_response(mocked_host) } + let!(:host_request) { mock_server_config_check_response(mocked_host) } + + # As the NextcloudContract is selected by the BaseContract to make writable attributes available, + # the BaseContract needs to be instantiated here. + subject { Storages::BaseContract.new(storage, current_user) } + + it "checks the storage url only when changed" do + subject.validate + expect(capabilities_request).to have_been_made.once + expect(host_request).to have_been_made.once + + WebMock.reset_executed_requests! + storage.save + subject.validate + expect(capabilities_request).not_to have_been_made + expect(host_request).not_to have_been_made + end + + describe "Nextcloud application credentials validation" do + context "with valid credentials" do + let(:storage) { build(:nextcloud_storage, :as_automatically_managed) } + + it "passes validation" do + credentials_request = mock_nextcloud_application_credentials_validation(storage.host) + + expect(subject).to be_valid + expect(credentials_request).to have_been_made.once + end + + context "with invalid credentials" do + let(:storage) { build(:nextcloud_storage, :as_automatically_managed) } + + it "fails validation" do + credentials_request = mock_nextcloud_application_credentials_validation(storage.host, response_code: 401) + + expect(subject).not_to be_valid + expect(subject.errors.to_hash).to eq({ password: ["is not valid."] }) + + expect(credentials_request).to have_been_made.once + end + end + + context "with timeout" do + let(:storage) { build(:nextcloud_storage, :as_automatically_managed) } + + it "fails validation" do + credentials_request = mock_nextcloud_application_credentials_validation(storage.host, timeout: true) + + expect(subject).not_to be_valid + expect(subject.errors.to_hash) + .to eq({ password: ["could not be validated. Please check your storage connection and try again."] }) + + # twice due to HTTPX retry plugin being enabled. + expect(credentials_request).to have_been_made.twice + end + end + + context "with unknown error" do + let(:storage) { build(:nextcloud_storage, :as_automatically_managed) } + + it "fails validation" do + credentials_request = mock_nextcloud_application_credentials_validation(storage.host, response_code: 500) + + expect(subject).not_to be_valid + expect(subject.errors.to_hash) + .to eq({ password: ["could not be validated. Please check your storage connection and try again."] }) + + expect(credentials_request).to have_been_made.once + end + end + + context "when the storage is not automatically managed" do + let(:storage) { build(:nextcloud_storage, :as_not_automatically_managed) } + + it "skips credentials validation" do + credentials_request = mock_nextcloud_application_credentials_validation(storage.host) + + expect(subject).to be_valid + expect(credentials_request).not_to have_been_made + end + end + + context "when the storage host has a subpath" do + let(:storage) { build(:nextcloud_storage, :as_automatically_managed, host: "https://host1.example.com/api") } + + it "passes validation" do + credentials_request = mock_nextcloud_application_credentials_validation(storage.host) + + expect(subject).to be_valid + expect(credentials_request).to have_been_made.once + end + end + end + + context "when the storage host is nil" do + let(:storage) { build(:nextcloud_storage, :as_automatically_managed, host: nil) } + let(:mocked_host) { "https://example.com/unrelated" } + + before do + allow(NextcloudApplicationCredentialsValidator).to receive(:new).and_call_original + end + + it "fails validation" do + expect(subject).not_to be_valid + expect(subject.errors.to_hash).to eq({ host: ["is not a valid URL."] }) + expect(NextcloudApplicationCredentialsValidator).not_to have_received(:new) + end + end + end + + describe "authentication_method validation" do + let(:storage) { build(:nextcloud_storage, :as_not_automatically_managed, authentication_method:) } + let(:authentication_method) { "two_way_oauth2" } + + it { is_expected.to be_valid } + + context "when the authentication method is oauth2_sso" do + let(:authentication_method) { "oauth2_sso" } + + before { storage.storage_audience = "valid_audience" } + + it { is_expected.not_to be_valid } + + context "and there is a valid enterprise token", with_ee: [:nextcloud_sso] do + it { is_expected.to be_valid } + end + + context "and the authentication_method has been oauth2_sso before" do + before do + storage.save! # storage is already persisted with this auth method + end + + it { is_expected.to be_valid } + end + end + + context "when the authentication method is unknown" do + let(:authentication_method) { "magic_unicorns" } + + it { is_expected.not_to be_valid } + end + + context "when the authentication method is missing" do + let(:authentication_method) { nil } + + it { is_expected.not_to be_valid } + end + end + + describe "storage_audience validation" do + let(:storage) do + build(:nextcloud_storage, :as_not_automatically_managed, authentication_method:, storage_audience:) + end + + context "when authentication happens through bidirectional OAuth 2.0" do + let(:authentication_method) { "two_way_oauth2" } + + context "and there is no storage_audience" do + let(:storage_audience) { nil } + + it { is_expected.to be_valid } + end + + context "and there is a storage_audience" do + let(:storage_audience) { "nextcloud" } + + it { is_expected.to be_valid } + end + end + + context "when authentication happens through a common IDP", with_ee: [:nextcloud_sso] do + let(:authentication_method) { "oauth2_sso" } + + context "and there is no storage_audience" do + let(:storage_audience) { nil } + + it { is_expected.not_to be_valid } + end + + context "and there is a storage_audience" do + let(:storage_audience) { "nextcloud" } + + it { is_expected.to be_valid } + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/capabilities_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/capabilities_query_spec.rb new file mode 100644 index 00000000000..bf8ab34d74e --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/capabilities_query_spec.rb @@ -0,0 +1,113 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe CapabilitiesQuery, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + + let(:auth_strategy) { Input::Strategy.build(key: :noop) } + + it "is registered as queries.capabilities" do + expect(Registry.resolve("nextcloud.queries.capabilities")).to eq(described_class) + end + + it "responds to #call with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy]) + end + + shared_examples_for "a successful Nextcloud capabilities response" do + it "returns a capabilities object" do + result = described_class.call(storage:, auth_strategy:) + expect(result).to be_success + + response = result.value! + expect(response).to be_a(ProviderResults::Capabilities) + expect(response.app_enabled?).to eq(app_enabled?) + expect(response.app_version).to eq(app_version) + expect(response.group_folder_enabled?).to eq(group_folder_enabled?) + expect(response.group_folder_version).to eq(group_folder_version) + end + end + + context "if both apps are installed", vcr: "nextcloud/capabilities_success" do + let(:app_enabled?) { true } + let(:app_version) { SemanticVersion.parse("2.6.3") } + let(:group_folder_enabled?) { true } + let(:group_folder_version) { SemanticVersion.parse("16.0.7") } + + it_behaves_like "a successful Nextcloud capabilities response" + end + + context "if group folder app is installed but disabled", + vcr: "nextcloud/capabilities_success_group_folder_disabled" do + let(:app_enabled?) { true } + let(:app_version) { SemanticVersion.parse("2.6.3") } + let(:group_folder_enabled?) { false } + let(:group_folder_version) { SemanticVersion.parse("16.0.7") } + + it_behaves_like "a successful Nextcloud capabilities response" + end + + context "if group folder app is not installed", vcr: "nextcloud/capabilities_success_group_folder_not_installed" do + let(:app_enabled?) { true } + let(:app_version) { SemanticVersion.parse("2.6.3") } + let(:group_folder_enabled?) { false } + let(:group_folder_version) { nil } + + it_behaves_like "a successful Nextcloud capabilities response" + end + + context "if integration app is not installed", vcr: "nextcloud/capabilities_success_app_disabled" do + let(:app_enabled?) { false } + let(:app_version) { nil } + let(:group_folder_enabled?) { false } + let(:group_folder_version) { nil } + + it_behaves_like "a successful Nextcloud capabilities response" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/download_link_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/download_link_query_spec.rb new file mode 100644 index 00000000000..d17f4c5e878 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/download_link_query_spec.rb @@ -0,0 +1,101 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe DownloadLinkQuery, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + let(:auth_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + + let(:file_link) { create(:file_link, origin_id: "182") } + let(:not_existent_file_link) { create(:file_link, origin_id: "DeathStarNumberThree") } + let(:input_data) { Input::DownloadLink.build(file_link:).value! } + + subject { described_class.new(storage) } + + describe "#call" do + it "responds with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) + end + + context "with outbound request successful" do + it "returns a result with a download url", vcr: "nextcloud/download_link_query_success" do + download_link = subject.call(auth_strategy:, input_data:) + + expect(download_link).to be_success + + uri = download_link.value! + expect(uri.host).to eq("nextcloud.local") + expect(uri.path) + .to match(/index.php\/apps\/integration_openproject\/direct\/[0-9a-zA-Z]+\/#{file_link.origin_name}/) + end + + it "returns an error if the file is not found", vcr: "nextcloud/download_link_query_not_found" do + input_data = Input::DownloadLink.build(file_link: not_existent_file_link).value! + download_link = subject.call(auth_strategy:, input_data:) + + expect(download_link).to be_failure + + error = download_link.failure + expect(error.source).to eq(described_class) + expect(error.code).to eq(:not_found) + end + end + + context "with outbound request returning 200 and an empty body" do + it "refreshes the token and returns success", vcr: "nextcloud/download_link_query_unauthorized" do + download_link = subject.call(auth_strategy:, input_data:) + expect(download_link).to be_success + + uri = download_link.value! + expect(uri.host).to eq("nextcloud.local") + expect(uri.path) + .to match(/index.php\/apps\/integration_openproject\/direct\/[0-9a-zA-Z]+\/#{file_link.origin_name}/) + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/file_info_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/file_info_query_spec.rb new file mode 100644 index 00000000000..2b9470e600c --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/file_info_query_spec.rb @@ -0,0 +1,133 @@ +# 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" +require_module_spec_helper +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe FileInfoQuery, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + let(:auth_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + let(:input_data) { Input::FileInfo.build(file_id:).value! } + + it_behaves_like "adapter file_info_query: basic query setup" + + context "with a file id requested", vcr: "nextcloud/file_info_query_success_file" do + let(:file_id) { "267" } + let(:file_info) do + Results::StorageFileInfo.new( + id: file_id, + status: "ok", + status_code: 200, + name: "android-studio-linux.tar.gz", + size: 982713473, + mime_type: "application/gzip", + created_at: Time.parse("1970-01-01T00:00:00Z"), + last_modified_at: Time.parse("2022-12-01T07:43:36Z"), + owner_name: "admin", + owner_id: "admin", + last_modified_by_name: nil, + last_modified_by_id: nil, + permissions: "RGDNVW", + location: "/My%20files/android-studio-linux.tar.gz" + ) + end + + it_behaves_like "adapter file_info_query: successful file/folder response" + end + + context "with a folder id requested", vcr: "nextcloud/file_info_query_success_folder" do + let(:file_id) { "350" } + let(:file_info) do + Results::StorageFileInfo.new( + id: file_id, + status: "ok", + status_code: 200, + name: "Ümlæûts", + size: 19720, + mime_type: "application/x-op-directory", + created_at: Time.parse("1970-01-01T00:00:00Z"), + last_modified_at: Time.parse("2024-04-29T09:21:03Z"), + owner_name: "admin", + owner_id: "admin", + last_modified_by_name: nil, + last_modified_by_id: nil, + permissions: "RGDNVCK", + location: "/Folder/%C3%9Cml%C3%A6%C3%BBts" + ) + end + + it_behaves_like "adapter file_info_query: successful file/folder response" + end + + context "with a file with special characters in the path", + vcr: "nextcloud/file_info_query_success_special_characters" do + let(:file_id) { "361" } + let(:file_info) do + Results::StorageFileInfo.new( + id: file_id, + status: "ok", + status_code: 200, + name: "what_have_you_done.md", + size: 0, + mime_type: "text/markdown", + created_at: Time.parse("1970-01-01T00:00:00Z"), + last_modified_at: Time.parse("2024-06-17T09:51:59Z"), + owner_name: "admin", + owner_id: "admin", + last_modified_by_name: nil, + last_modified_by_id: nil, + permissions: "RGDNVW", + location: + "/Folder%20with%20spaces/%C3%9Cml%C3%A4uts%20%26%20spe%C2%A2i%C3%A6l%20characters/what_have_you_done.md" + ) + end + + it_behaves_like "adapter file_info_query: successful file/folder response" + end + + context "with a not existing file id", vcr: "nextcloud/file_info_query_not_found" do + let(:file_id) { "not_existent" } + let(:error_source) { described_class } + + it_behaves_like "adapter file_info_query: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/file_path_to_id_map_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/file_path_to_id_map_query_spec.rb new file mode 100644 index 00000000000..f2a435b847f --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/file_path_to_id_map_query_spec.rb @@ -0,0 +1,129 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe FilePathToIdMapQuery, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + + let(:auth_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + let(:depth) { Float::INFINITY } + let(:input_data) { Input::FilePathToIdMap.build(folder:, depth:).value! } + + it_behaves_like "adapter file_path_to_id_map_query: basic query setup" + + context "with parent folder being root" do + let(:folder) { "/" } + + context "with unset depth (defaults to INFINITY)", vcr: "nextcloud/file_path_to_id_map_query_root_depth_infinite" do + let(:expected_ids) do + { + "/" => "2", + "/Folder with spaces" => "165", + "/Folder with spaces/New Requests" => "166", + "/Folder with spaces/New Requests/I❤️you death star.md" => "167", + "/Folder with spaces/New Requests/request_002.md" => "168", + "/Folder with spaces/Ümläuts & spe¢iæl characters" => "360", + "/Folder with spaces/Ümläuts & spe¢iæl characters/what_have_you_done.md" => "361", + "/My files" => "169", + "/My files/android-studio-linux.tar.gz" => "267", + "/My files/empty" => "172", + "/My files/Ümlæûts" => "350", + "/My files/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "351", + "/Practical_guide_to_BAGGM_Digital.pdf" => "295", + "/Readme.md" => "268", + "/VCR" => "773", + "/VCR/placeholder" => "790" + } + end + + it_behaves_like "adapter file_path_to_id_map_query: successful query" + end + + context "with depth 0", vcr: "nextcloud/file_path_to_id_map_query_root_depth_0" do + let(:depth) { 0 } + let(:expected_ids) { { "/" => "2" } } + + it_behaves_like "adapter file_path_to_id_map_query: successful query" + end + + context "with depth 1", vcr: "nextcloud/file_path_to_id_map_query_root_depth_1" do + let(:depth) { 1 } + let(:expected_ids) do + { + "/" => "2", + "/Folder with spaces" => "165", + "/My files" => "169", + "/Practical_guide_to_BAGGM_Digital.pdf" => "295", + "/Readme.md" => "268", + "/VCR" => "773" + } + end + + it_behaves_like "adapter file_path_to_id_map_query: successful query" + end + end + + context "with a given parent folder", vcr: "nextcloud/file_path_to_id_map_query_parent_folder" do + let(:folder) { "/Folder" } + let(:expected_ids) do + { + "/Folder" => "169", + "/Folder/android-studio-2021.3.1.17-linux.tar.gz" => "267", + "/Folder/empty" => "172", + "/Folder/Ümlæûts" => "350", + "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "351" + } + end + + it_behaves_like "adapter file_path_to_id_map_query: successful query" + end + + context "with not existent parent folder", vcr: "nextcloud/file_path_to_id_map_query_invalid_parent" do + let(:folder) { "/I/just/made/that/up" } + let(:error_source) { PropfindQuery } + + it_behaves_like "adapter file_path_to_id_map_query: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/files_info_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/files_info_query_spec.rb new file mode 100644 index 00000000000..fe721dac8a0 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/files_info_query_spec.rb @@ -0,0 +1,115 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe FilesInfoQuery, :webmock do + let(:user) { create(:user) } + let(:auth_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + + let(:input_data) { Input::FilesInfo.build(file_ids:).value! } + + subject { described_class.new(storage) } + + describe "#call" do + let(:file_ids) { %w[182 203 222] } + + context "without outbound request involved" do + context "with an empty array of file ids" do + let(:file_ids) { [] } + + it "returns an empty array" do + result = subject.call(auth_strategy:, input_data:) + + expect(result).to be_success + expect(result.value!).to eq([]) + end + end + end + + context "with outbound request successful", vcr: "nextcloud/files_info_query_success" do + context "with an array of file ids" do + it "must return an array of file information when called" do + result = subject.call(auth_strategy:, input_data:) + expect(result).to be_success + + file_infos = result.value! + expect(file_infos.size).to eq(3) + expect(file_infos).to all(be_a(Results::StorageFileInfo)) + end + end + end + + context "with outbound request not found" do + context "with a single file id", vcr: "nextcloud/files_info_query_not_found" do + let(:file_ids) { %w[1234] } + + it "returns an HTTP 200 with individual status code per file ID" do + result = subject.call(auth_strategy:, input_data:) + expect(result).to be_success + + file_infos = result.value! + expect(file_infos.size).to eq(1) + expect(file_infos.first.to_h).to include(status: "Not Found", status_code: 404) + end + end + end + + context "with outbound request not authorized" do + context "with multiple file IDs, one of which is not authorized", + vcr: "nextcloud/files_info_query_only_one_not_authorized" do + let(:file_ids) { %w[182 1234] } + + it "returns an HTTP 200 with individual status code per file ID" do + result = subject.call(auth_strategy:, input_data:) + expect(result).to be_success + + file_infos = result.value! + expect(file_infos.size).to eq(2) + expect(file_infos.map(&:status_code)).to contain_exactly(403, 404) + end + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/files_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/files_query_spec.rb new file mode 100644 index 00000000000..25944a959cb --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/files_query_spec.rb @@ -0,0 +1,254 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe FilesQuery, :vcr, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, + :as_not_automatically_managed, + oauth_client_token_user: user, + origin_user_id: "m.jade@death.star") + end + + let(:auth_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + let(:input_data) { Input::Files.build(folder:).value! } + + it_behaves_like "adapter files_query: basic query setup" + + context "with parent folder being root", vcr: "nextcloud/files_query_root" do + let(:folder) { "/" } + let(:files_result) do + # FIXME: nextcloud files query currently does not correctly returns modifier and creation date. + Results::StorageFileCollection.new( + [ + Results::StorageFile.new(id: "555", + name: "Folder", + size: 232167, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "561", + name: "Folder with spaces", + size: 890, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:52:09Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder%20with%20spaces", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "562", + name: "Ümlæûts", + size: 19720, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:51:48Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/%c3%9cml%c3%a6%c3%bbts", + permissions: %i[readable writeable]) + ], + Results::StorageFile.new(id: "385", + name: "Root", + size: 252777, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/", + permissions: %i[readable writeable]), + [] + ) + end + + it_behaves_like "adapter files_query: successful files response" + end + + context "with a given parent folder", vcr: "nextcloud/files_query_parent_folder" do + let(:folder) { "/Folder/Nested Folder" } + let(:files_result) do + Results::StorageFileCollection.new( + [ + Results::StorageFile.new(id: "603", + name: "giphy.gif", + size: 184726, + mime_type: "image/gif", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:53:24Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder/Nested%20Folder/giphy.gif", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "604", + name: "release_meme.jpg", + size: 46264, + mime_type: "image/jpeg", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:53:30Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder/Nested%20Folder/release_meme.jpg", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "602", + name: "todo.txt", + size: 55, + mime_type: "text/plain", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:53:35Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder/Nested%20Folder/todo.txt", + permissions: %i[readable writeable]) + ], + Results::StorageFile.new(id: "601", + name: "Nested Folder", + size: 231045, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder/Nested%20Folder", + permissions: %i[readable writeable]), + [ + Results::StorageFile.new(id: "8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1", + name: "Root", + location: "/"), + Results::StorageFile.new(id: "0da2f1cf70005eaeb08333802726c2928503d975e4a70cbdd1a28313cded20ae", + name: "Folder", + location: "/Folder") + ] + ) + end + + it_behaves_like "adapter files_query: successful files response" + end + + context "with parent folder being empty", vcr: "nextcloud/files_query_empty_folder" do + let(:folder) { "/Folder with spaces/very empty folder" } + let(:files_result) do + Results::StorageFileCollection.new( + [], + Results::StorageFile.new(id: "571", + name: "very empty folder", + size: 0, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:52:04Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder%20with%20spaces/very%20empty%20folder", + permissions: %i[readable writeable]), + [ + Results::StorageFile.new(id: "8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1", + name: "Root", + location: "/"), + Results::StorageFile.new(id: "c8776f1f6dd36c023c6615d39f01a71d68dd1707b232115b7a4f58bc6da94e2e", + name: "Folder with spaces", + location: "/Folder%20with%20spaces") + ] + ) + end + + it_behaves_like "adapter files_query: successful files response" + end + + context "with a path full of umlauts", vcr: "nextcloud/files_query_umlauts" do + let(:folder) { "/Ümlæûts" } + let(:files_result) do + Results::StorageFileCollection.new( + [ + Results::StorageFile.new(id: "564", + name: "Anrüchiges deutsches Dokument.docx", + size: 19720, + mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:51:40Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/%c3%9cml%c3%a6%c3%bbts/Anr%c3%bcchiges%20deutsches%20Dokument.docx", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "563", + name: "data", + size: 0, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:51:30Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/%c3%9cml%c3%a6%c3%bbts/data", + permissions: %i[readable writeable]) + ], + Results::StorageFile.new(id: "562", + name: "Ümlæûts", + size: 19720, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:51:48Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/%c3%9cml%c3%a6%c3%bbts", + permissions: %i[readable writeable]), + [ + Results::StorageFile.new(id: "8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1", + name: "Root", + location: "/") + ] + ) + end + + it_behaves_like "adapter files_query: successful files response" + end + + context "with not existent parent folder", vcr: "nextcloud/files_query_invalid_parent" do + let(:folder) { "/I/just/made/that/up" } + let(:error_source) { described_class } + + it_behaves_like "adapter files_query: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/group_users_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/group_users_query_spec.rb new file mode 100644 index 00000000000..7e855f67139 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/group_users_query_spec.rb @@ -0,0 +1,125 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe GroupUsersQuery, :webmock do + let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed, username: "vcr") } + let(:auth_strategy) { Registry.resolve("nextcloud.authentication.userless").call } + + let(:input_data) { Input::GroupUsers.build(group:).value! } + + describe "basic command setup" do + it "is registered as queries.group_users" do + expect(Registry.resolve("#{storage}.queries.group_users")).to eq(described_class) + end + + it "responds to #call with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy], + %i[keyreq input_data]) + end + end + + context "if group exists", vcr: "nextcloud/group_users_success" do + let(:user1) { "m.jade@death.star" } + let(:user2) { "d.vader@death.star" } + let(:user3) { "l.organa@filthy.rebels" } + let(:group) { "Sith Assassins" } + + before do + create_group(auth, storage, group) + add_user_to_group(user1, group) + add_user_to_group(user2, group) + end + + after do + remove_group(auth, storage, group) + end + + it "returns a success" do + result = described_class.call(storage:, auth_strategy:, input_data:) + expect(result).to be_success + expect(result.value!).to include(user1, user2) + expect(result.value!).not_to include(user3) + end + end + + context "if group does not exist", vcr: "nextcloud/group_users_not_existing_group" do + let(:user) { "m.jade@death.star" } + let(:group) { "Sith Assassins" } + + it "returns a failure" do + result = described_class.call(storage:, auth_strategy:, input_data:) + expect(result).to be_failure + + error = result.failure + expect(error.code).to eq(:group_does_not_exist) + expect(error.source).to eq(described_class) + end + end + + private + + def auth = Authentication[auth_strategy] + + def add_user_to_group(user, group) + Input::AddUserToGroup.build(user:, group:).bind do |input_data| + Commands::AddUserToGroupCommand.call(storage:, auth_strategy:, input_data:) + end + end + + def create_group(authentication, storage, group) + authentication.call(storage:, http_options: { headers: { "OCS-APIRequest" => "true" } }) do |http| + http.post(UrlBuilder.url(storage.uri, "ocs/v1.php/cloud/groups"), + form: { "groupid" => group }) + end + end + + def remove_group(authentication, storage, group) + authentication.call(storage:, http_options: { headers: { "OCS-APIRequest" => "true" } }) do |http| + http.delete(UrlBuilder.url(storage.uri, "/ocs/v1.php/cloud/groups", group)) + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/open_file_link_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/open_file_link_query_spec.rb new file mode 100644 index 00000000000..35ed75b4412 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/open_file_link_query_spec.rb @@ -0,0 +1,78 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe OpenFileLinkQuery do + let(:storage) { create(:nextcloud_storage, host: "https://example.com") } + let(:file_id) { "1337" } + let(:auth_strategy) { Registry["nextcloud.authentication.userless"].call } + let(:input_data) { Input::OpenFileLink.build(file_id:).value! } + + it "responds to .call" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy], + %i[keyreq input_data]) + end + + it "returns the url for opening the file on storage" do + url = described_class.call(storage:, auth_strategy:, input_data:).value! + expect(url).to eq("#{storage.host}/index.php/f/#{file_id}?openfile=1") + end + + it "returns the url for opening the file's location on storage" do + input_data = Input::OpenFileLink.build(file_id:, open_location: true).value! + url = described_class.call(storage:, auth_strategy:, input_data:).value! + expect(url).to eq("#{storage.host}/index.php/f/#{file_id}?openfile=0") + end + + context "with a storage with host url with a sub path" do + let(:storage) { create(:nextcloud_storage, host: "https://example.com/html") } + + it "returns the url for opening the file on storage" do + url = described_class.call(storage:, auth_strategy:, input_data:).value! + expect(url).to eq("#{storage.host}/index.php/f/#{file_id}?openfile=1") + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/open_storage_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/open_storage_query_spec.rb new file mode 100644 index 00000000000..5ca459026d8 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/open_storage_query_spec.rb @@ -0,0 +1,67 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe OpenStorageQuery do + let(:storage) { create(:nextcloud_storage, host: "https://example.com") } + + it "responds to .call" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) + end + + it "returns the url for opening the file on storage" do + url = described_class.call(storage:, auth_strategy: nil, input_data: nil).value! + expect(url).to eq("#{storage.host}/index.php/apps/files") + end + + context "with a storage with host url with a sub path" do + let(:storage) { create(:nextcloud_storage, host: "https://example.com/html") } + + it "returns the url for opening the file on storage" do + url = described_class.call(storage:, auth_strategy: nil, input_data: nil).value! + expect(url).to eq("#{storage.host}/index.php/apps/files") + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/upload_link_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/upload_link_query_spec.rb new file mode 100644 index 00000000000..179b61559ac --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/upload_link_query_spec.rb @@ -0,0 +1,74 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe UploadLinkQuery, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) + end + let(:auth_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + + it_behaves_like "adapter upload_link_query: basic query setup" + + context "when requesting an upload link for an existing file", vcr: "nextcloud/upload_link_success" do + let(:input_data) do + Input::UploadLink.build(folder_id: "169", file_name: "DeathStart_blueprints.tiff").value! + end + + let(:token) { "SrQJeC5zM3B5Gw64d7dEQFQpFw8YBAtZWoxeLb59AR7PpGPyoGAkAko5G6ZiZ2HA" } + let(:upload_url) { "https://nextcloud.local/index.php/apps/integration_openproject/direct-upload/#{token}" } + let(:upload_method) { :post } + + it_behaves_like "adapter upload_link_query: successful upload link response" + end + + context "when requesting an upload link for a not existing file", vcr: "nextcloud/upload_link_not_found" do + let(:input_data) do + Input::UploadLink.build(folder_id: "1337", file_name: "DeathStart_blueprints.tiff").value! + end + + let(:error_source) { described_class } + + it_behaves_like "adapter upload_link_query: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/user_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/user_query_spec.rb new file mode 100644 index 00000000000..f584f66072e --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/queries/user_query_spec.rb @@ -0,0 +1,79 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Queries + RSpec.describe UserQuery, :webmock do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_automatically_managed, + username: "vcr", oauth_client_token_user: user) + end + + let(:userless_strategy) { Registry["nextcloud.authentication.userless"].call } + let(:user_bound_strategy) { Registry["nextcloud.authentication.user_bound"].call(user, storage) } + + it "is registered" do + expect(Registry.resolve("#{storage}.queries.user")).to eq(described_class) + end + + it "responds to #call with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy]) + end + + it "responds with failure with invalid token", vcr: "nextcloud/user_query_unauthorized" do + result = described_class.call(storage:, auth_strategy: userless_strategy) + + expect(result).to be_failure + expect(result.failure.code).to eq(:unauthorized) + end + + it "responds with success with valid token", vcr: "nextcloud/user_query_success" do + result = described_class.call(storage:, auth_strategy: user_bound_strategy) + + expect(result).to be_success + expect(result.value!).to eq(id: "admin") + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/peripherals/nextcloud_storage_wizard_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/storage_wizard_spec.rb similarity index 99% rename from modules/storages/spec/common/storages/peripherals/nextcloud_storage_wizard_spec.rb rename to modules/storages/spec/common/storages/adapters/providers/nextcloud/storage_wizard_spec.rb index 041b015aa92..d83b5646115 100644 --- a/modules/storages/spec/common/storages/peripherals/nextcloud_storage_wizard_spec.rb +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/storage_wizard_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" -RSpec.describe Storages::Peripherals::NextcloudStorageWizard do +RSpec.describe Storages::Adapters::Providers::Nextcloud::StorageWizard do subject(:wizard) { described_class.new(model:, user:) } let(:model) { Storages::NextcloudStorage.new } diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/ampf_configuration_validator_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/ampf_configuration_validator_spec.rb new file mode 100644 index 00000000000..2d37179a8e7 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/ampf_configuration_validator_spec.rb @@ -0,0 +1,173 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Validators + RSpec.describe AmpfConfigurationValidator, :webmock do + let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed) } + let(:project_folder_id) { "1337" } + let!(:project_storage) do + create(:project_storage, :as_automatically_managed, project_folder_id:, storage:, project: create(:project)) + end + + let(:files_response) do + Success(Results::StorageFileCollection.new( + files: [StorageFile.new(id: project_folder_id, name: project_storage.managed_project_folder_name)], + parent: StorageFile.new(id: "root", name: "root"), + ancestors: [] + )) + end + + let(:required_versions) do + YAML.load_file(Rails.root.join("modules/storages/config/nextcloud_dependencies.yml"))&.dig("dependencies") + end + + let(:capabilities_response) do + ProviderResults::Capabilities.build( + app_enabled: true, + app_version: SemanticVersion.parse(required_versions.dig("group_folders_app", "min_version")), + group_folder_enabled: true, + group_folder_version: SemanticVersion.parse(required_versions.dig("group_folders_app", "min_version")) + ) + end + + subject(:validator) { described_class.new(storage) } + + before do + Registry.stub("nextcloud.queries.capabilities", ->(*) { capabilities_response }) + Registry.stub("nextcloud.queries.files", ->(*) { files_response }) + end + + it "pass all checks" do + expect(validator.call).to be_success + end + + describe "group_folders_app checks" do + before do + Registry.unstub + Registry.stub("nextcloud.queries.files", ->(*) { files_response }) + end + + it "group_folders_app version mismatch", vcr: "nextcloud/capabilities_success" do + absurd_version = { dependencies: { group_folders_app: { min_version: "2099.10.138" } } }.deep_stringify_keys + allow(subject).to receive(:nextcloud_dependencies).and_return(absurd_version) + + results = validator.call + expect(results[:group_folder_app]).to be_a_failure + expect(results[:group_folder_app].code).to eq(:nc_dependency_version_mismatch) + expect(results[:group_folder_app].context[:dependency]).to eq("Group Folders") + end + + it "integration app disabled / missing", vcr: "nextcloud/capabilities_success_group_folder_disabled" do + results = validator.call + + expect(results[:group_folder_app]).to be_a_failure + expect(results[:group_folder_app].code).to eq(:nc_dependency_missing) + expect(results[:group_folder_app].context[:dependency]).to eq("Group Folders") + end + end + + context "if userless authentication fails" do + let(:files_response) { build_failure(code: :unauthorized, payload: nil) } + + it "fails and skips the next checks" do + results = validator.call + + states = results.tally + expect(states).to eq({ success: 2, failure: 1, skipped: 2 }) + expect(results[:userless_access]).to be_failure + expect(results[:userless_access].code).to eq(:nc_userless_access_denied) + end + end + + context "if the files request returns not_found" do + let(:files_response) { build_failure(code: :not_found, payload: nil) } + + it "fails the check" do + results = validator.call + + expect(results[:group_folder_presence]).to be_failure + expect(results[:group_folder_presence].code).to eq(:nc_group_folder_not_found) + end + end + + context "if the files request returns an unknown error" do + let(:files_response) { build_failure(code: :error) } + + before { allow(Rails.logger).to receive(:error) } + + it "fails the check and logs the error" do + results = validator.call + + expect(results[:files_request]).to be_failure + expect(results[:files_request].code).to eq(:unknown_error) + + expect(Rails.logger).to have_received(:error).with(/Connection validation failed with unknown error/) + end + end + + context "if the files request returns unexpected files" do + let(:files_response) do + Success(Results::StorageFileCollection.new( + files: [ + StorageFile.new(id: project_folder_id, name: "I am your father"), + StorageFile.new(id: "noooooooooo", name: "testimony_of_luke_skywalker.md") + ], + parent: StorageFile.new(id: "root", name: "root"), + ancestors: [] + )) + end + + it "warns the user about extraneous folders" do + results = validator.call + + expect(results[:group_folder_contents]).to be_a_warning + expect(results[:group_folder_contents].code).to eq(:nc_unexpected_content) + end + end + + private + + def build_failure(code:, payload: nil) + error = Results::Error.new(code:, payload:, source: self) + Failure(error) + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/authentication_validator_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/authentication_validator_spec.rb new file mode 100644 index 00000000000..8536f105824 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/authentication_validator_spec.rb @@ -0,0 +1,235 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Validators + RSpec.describe AuthenticationValidator, :webmock do + subject(:validator) { described_class.new(storage) } + + context "when using OAuth2" do + let(:user) { create(:user) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, + oauth_client_token_user: user, origin_user_id: "m.jade@death.star") + end + + before { User.current = user } + + it "passes when the user has a token and the request works", vcr: "nextcloud/user_query_success" do + expect(validator.call).to be_success + end + + it "returns a warning when there's no token for the current user" do + User.current = create(:user) + result = validator.call + + expect(result[:existing_token]).to be_a_warning + expect(result[:existing_token].code).to eq(:nc_oauth_token_missing) + expect(result[:user_bound_request]).to be_skipped + end + + it "returns a failure if the remote call failed" do + error = Results::Error.new(code: :unauthorized, source: self) + Registry.stub("nextcloud.queries.user", ->(_) { Failure(error) }) + + result = validator.call + expect(result[:user_bound_request]).to be_a_failure + expect(result[:user_bound_request].code).to eq(:nc_oauth_request_unauthorized) + end + end + + context "when using OpenID Connect" do + let(:storage) { create(:nextcloud_storage_configured, :oidc_sso_enabled) } + + let(:user) { create(:user, identity_url: "#{oidc_provider.slug}:123123123123") } + let!(:oidc_provider) { create(:oidc_provider, scope:) } + let!(:saml_provider) { create(:saml_provider) } + let(:scope) { "openid email profile offline_access" } + + before do + User.current = user + + xml_response = Rails.root.join("modules/storages/spec/support/payloads/nextcloud_user_query_success.xml") + stub_request(:get, "#{storage.uri}ocs/v1.php/cloud/user") + .and_return(status: 200, body: File.read(xml_response), headers: { content_type: "text/xml" }) + end + + it "succeeds give the user is provisioned and tokens can be acquired" do + create(:oidc_user_token, user:, extra_audiences: storage.audience) + expect(validator.call).to be_success + end + + describe "error and warning handling" do + it "returns a warning if the current user isn't provisioned" do + user.user_auth_provider_links.destroy_all + result = validator.call + + expect(result[:non_provisioned_user]).to be_warning + expect(result[:non_provisioned_user].code).to eq(:oidc_non_provisioned_user) + + state_count = result.tally + expect(state_count).to eq({ skipped: 4, warning: 1 }) + end + + it "returns a warning if the user is not provisioned by an oidc provider" do + link = user.user_auth_provider_links.first + link.update!(auth_provider_id: saml_provider.id) + + result = validator.call + + expect(result[:provisioned_user_provider]).to be_warning + expect(result[:provisioned_user_provider].code).to eq(:oidc_non_oidc_user) + + state_count = result.tally + expect(state_count).to eq({ success: 1, skipped: 3, warning: 1 }) + end + + context "when the offline_access scope is not configured" do + let(:scope) { "openid email profile" } + + it "returns a warning", :aggregate_failures do + create(:oidc_user_token, user:, extra_audiences: storage.audience) + result = validator.call + + expect(result[:offline_access]).to be_warning + expect(result[:offline_access].code).to eq(:offline_access_scope_missing) + + state_count = result.tally + expect(state_count).to eq({ success: 4, warning: 1 }) + end + end + end + + describe "checks related to the token" do + context "when the token doesn't have the necessary audiences" do + it "returns a validation failure in case the server does not support token exchange" do + create(:oidc_user_token, user:) + result = validator.call + + expect(result[:token_negotiable]).to be_failure + expect(result[:token_negotiable].code).to eq(:oidc_token_acquisition_failed) + end + end + + context "when the existing token requires a refresh" do + let(:expired_storage_token) do + create(:oidc_user_token, user:, extra_audiences: storage.audience, expires_at: 10.hours.ago) + end + + it "tries to refresh the token if it is expired" do + refresh_request = stub_request(:post, oidc_provider.token_endpoint) + .with(body: { grant_type: "refresh_token", + refresh_token: expired_storage_token.refresh_token }) + .and_return_json(status: 200, body: { access_token: "NEW_TOKEN" }) + + expect(validator.call).to be_success + expect(refresh_request).to have_been_requested.once + end + + it "fails when the refresh response is invalid" do + stub_request(:post, oidc_provider.token_endpoint) + .with(body: { grant_type: "refresh_token", refresh_token: expired_storage_token.refresh_token }) + .and_return_json(status: 200, body: { error: "this is a broken endpoint" }) + + result = validator.call + + expect(result[:token_negotiable]).to be_failure + expect(result[:token_negotiable].code).to eq(:oidc_token_refresh_failed) + end + + it "fails when refresh fails" do + stub_request(:post, oidc_provider.token_endpoint) + .with(body: { grant_type: "refresh_token", refresh_token: expired_storage_token.refresh_token }) + .and_return(status: 401) + + result = validator.call + + expect(result[:token_negotiable]).to be_failure + expect(result[:token_negotiable].code).to eq(:oidc_token_refresh_failed) + end + + context "when the server supports token exchange" do + let(:oidc_provider) { create(:oidc_provider, :token_exchange_capable, scope: "offline_access") } + let!(:exchangeable_token) { create(:oidc_user_token, user:, refresh_token: nil) } + + it "favors token exchange when refreshing" do + exchange_request = stub_request(:post, oidc_provider.token_endpoint) + .with(body: hash_including( + grant_type: OpenProject::OpenIDConnect::TOKEN_EXCHANGE_GRANT_TYPE + )) + .and_return_json(status: 200, body: { access_token: "NEW_TOKEN" }) + + expect(validator.call).to be_success + expect(exchange_request).to have_been_requested.once + end + + it "fails if the exchange is met with an unexpected body" do + exchange_request = stub_request(:post, oidc_provider.token_endpoint) + .with(body: hash_including( + grant_type: OpenProject::OpenIDConnect::TOKEN_EXCHANGE_GRANT_TYPE + )) + .and_return_json(status: 200, body: { error: "failed " }) + + result = validator.call + + expect(result[:token_negotiable]).to be_failure + expect(result[:token_negotiable].code).to eq(:oidc_token_exchange_failed) + expect(exchange_request).to have_been_requested.once + end + + it "fails if the exchange fails" do + exchange_request = stub_request(:post, oidc_provider.token_endpoint) + .with(body: hash_including( + grant_type: OpenProject::OpenIDConnect::TOKEN_EXCHANGE_GRANT_TYPE + )) + .and_return(status: 401) + + result = validator.call + + expect(result[:token_negotiable]).to be_failure + expect(result[:token_negotiable].code).to eq(:oidc_token_exchange_failed) + expect(exchange_request).to have_been_requested.once + end + end + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/storage_configuration_validator_spec.rb b/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/storage_configuration_validator_spec.rb new file mode 100644 index 00000000000..390383f3c9a --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/nextcloud/validators/storage_configuration_validator_spec.rb @@ -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. +#++ + +require "spec_helper" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module Nextcloud + module Validators + RSpec.describe StorageConfigurationValidator, :webmock do + let(:storage) { create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed) } + + subject(:validator) { described_class.new(storage) } + + it "returns a GroupValidationResult", vcr: "nextcloud/capabilities_success" do + results = validator.call + + expect(results).to be_a(ConnectionValidators::ValidationGroupResult) + expect(results).to be_success + end + + describe "possible error scenarios" do + context "when the storage is not configured" do + let(:storage) { create(:nextcloud_storage) } + + it "the check fails" do + results = validator.call + expect(results[:storage_configured]).to be_a_failure + expect(results[:storage_configured].code).to eq(:not_configured) + end + end + + it "base url could not be reached" do + stub_request(:get, UrlBuilder.url(storage.uri, "/ocs/v2.php/cloud/capabilities")) + .to_return(status: 404, body: "Not Found") + + results = validator.call + expect(results[:host_url_accessible]).to be_a_failure + expect(results[:host_url_accessible].code).to eq(:nc_host_not_found) + end + + it "integration app version mismatch", vcr: "nextcloud/capabilities_success" do + absurd_version = { dependencies: { integration_app: { min_version: "2099.10.138" } } }.deep_stringify_keys + allow(subject).to receive(:nextcloud_dependencies).and_return(absurd_version) + + results = validator.call + expect(results[:dependencies_versions]).to be_a_failure + expect(results[:dependencies_versions].code).to eq(:nc_dependency_version_mismatch) + expect(results[:dependencies_versions].context[:dependency]).to eq("Integration OpenProject") + end + + it "integration app disabled / missing", vcr: "nextcloud/capabilities_success_app_disabled" do + results = validator.call + + expect(results[:dependencies_check]).to be_a_failure + expect(results[:dependencies_check].code).to eq(:nc_dependency_missing) + expect(results[:dependencies_check].context[:dependency]).to eq("Integration OpenProject") + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/copy_template_folder_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/copy_template_folder_command_spec.rb new file mode 100644 index 00000000000..d4b2bbeeede --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/copy_template_folder_command_spec.rb @@ -0,0 +1,193 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Commands + RSpec.describe CopyTemplateFolderCommand, :webmock do + shared_let(:storage) { create(:sharepoint_dev_drive_storage) } + + shared_let(:original_folders) do + use_storages_vcr_cassette("one_drive/copy_template_folder_existing_folders") { existing_folder_tuples } + end + + shared_let(:base_template_folder) do + use_storages_vcr_cassette("one_drive/copy_template_folder_base_folder") { create_base_folder } + end + + shared_let(:source) { base_template_folder.id } + + let(:input_data) { Input::CopyTemplateFolder.build(source:, destination:).value! } + + it "is registered under commands.one_drive.copy_template_folder" do + expect(Registry.resolve("one_drive.commands.copy_template_folder")).to eq(described_class) + end + + it "responds to .call" do + expect(described_class).to respond_to(:call) + end + + describe "#call" do + let(:destination) { "My New Folder" } + + # rubocop:disable RSpec/BeforeAfterAll + before(:all) do + use_storages_vcr_cassette("one_drive/copy_template_folder_setup") { setup_template_folder } + end + + after(:all) do + use_storages_vcr_cassette("one_drive/copy_template_folder_teardown") { delete_template_folder } + end + # rubocop:enable RSpec/BeforeAfterAll + + it "copies origin folder and all underlying files and folders to the destination_path", + vcr: "one_drive/copy_template_folder_copy_successful" do + command_result = described_class.call(auth_strategy:, storage:, input_data:) + + expect(command_result).to be_success + data = command_result.value! + + expect(data).to be_requires_polling + expect(data.polling_url).to match %r + ensure + delete_copied_folder(data.polling_url) + end + + describe "error handling" do + context "when the source_path does not exist" do + let(:source) { "TheCakeIsALie" } + let(:destination) { "Not Happening" } + + it "fails", vcr: "one_drive/copy_template_source_not_found" do + result = described_class.call(auth_strategy:, storage:, input_data:) + + expect(result).to be_failure + end + + it "explains the nature of the error", vcr: "one_drive/copy_template_source_not_found" do + result = described_class.call(auth_strategy:, storage:, input_data:) + + expect(result.failure.code).to eq(:not_found) + end + end + + context "when it would overwrite an already existing folder" do + let(:destination) { original_folders.first[:name] } + + it "fails", vcr: "one_drive/copy_template_folder_no_overwrite" do + result = described_class.call(auth_strategy:, storage:, input_data:) + + expect(result).to be_failure + end + + it "explains the nature of the error", vcr: "one_drive/copy_template_folder_no_overwrite" do + result = described_class.call(auth_strategy:, storage:, input_data:) + + expect(result.failure.code).to eq(:conflict) + end + end + end + end + + private + + def create_base_folder + Input::CreateFolder.build(folder_name: "Test Template Folder", parent_location: "/").bind do |input_data| + Registry.resolve("one_drive.commands.create_folder").call(storage:, auth_strategy:, input_data:).value! + end + end + + def setup_template_folder + raise if source.nil? + + command = Registry.resolve("one_drive.commands.create_folder").new(storage) + Input::CreateFolder.build(folder_name: "Empty Subfolder", parent_location: source).bind do |input_data| + command.call(auth_strategy:, input_data:) + + command.call(auth_strategy:, input_data: input_data.with(folder_name: "Subfolder with File")).bind do |subfolder| + file_name = "files_query_root.yml" + Input::UploadLink.build(folder_id: subfolder.id, file_name:).bind do |upload_data| + Registry.resolve("one_drive.queries.upload_link") + .call(storage:, auth_strategy:, input_data: upload_data).bind do |upload_link| + path = Rails.root.join("modules/storages/spec/support/fixtures/vcr_cassettes/one_drive", file_name) + File.open(path, "rb") do |file_handle| + HTTPX.with(headers: { content_length: file_handle.size, + "Content-Range" => "bytes 0-#{file_handle.size - 1}/#{file_handle.size}" }) + .put(upload_link.destination, body: file_handle.read).raise_for_status + end + end + end + end + end + end + + def delete_template_folder + Input::DeleteFolder + .build(location: base_template_folder.id) + .bind { Registry.resolve("one_drive.commands.delete_folder").call(storage:, auth_strategy:, input_data: it) } + end + + def existing_folder_tuples + Authentication[auth_strategy].call(storage:) do |http| + url = UrlBuilder.url(storage.uri, "/v1.0/drives", storage.drive_id, "/root/children") + response = http.get("#{url}?$select=name,id,folder") + + response.json(symbolize_keys: true).fetch(:value, []).filter_map do |item| + next unless item.key?(:folder) + + item.slice(:name, :id) + end + end + end + + def delete_copied_folder(url) + extractor_regex = /.+\/items\/(?\w+)\?/ + match_data = extractor_regex.match(url) + location = match_data[:item_id] + + Input::DeleteFolder + .build(location:) + .bind { Registry.resolve("one_drive.commands.delete_folder").call(storage:, auth_strategy:, input_data: it) } + end + + def auth_strategy + Registry["one_drive.authentication.userless"].call + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/create_folder_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/create_folder_command_spec.rb new file mode 100644 index 00000000000..ddb9f246a15 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/create_folder_command_spec.rb @@ -0,0 +1,88 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Commands + RSpec.describe CreateFolderCommand, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage) } + let(:auth_strategy) { Registry.resolve("one_drive.authentication.userless").call } + let(:input_data) { Input::CreateFolder.build(folder_name:, parent_location:).value! } + + it_behaves_like "adapter create_folder_command: basic command setup" + + context "when creating a folder in the root", vcr: "one_drive/create_folder_root" do + let(:folder_name) { "Földer CreatedBy Çommand" } + let(:parent_location) { "/" } + let(:path) { "/F%C3%B6lder%20CreatedBy%20%C3%87ommand" } + + it_behaves_like "adapter create_folder_command: successful folder creation" + end + + context "when creating a folder in a parent folder", vcr: "one_drive/create_folder_parent" do + let(:folder_name) { "Földer CreatedBy Çommand" } + let(:parent_location) { "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU" } + let(:path) { "/Folder%20with%20spaces/F%C3%B6lder%20CreatedBy%20%C3%87ommand" } + + it_behaves_like "adapter create_folder_command: successful folder creation" + end + + context "when creating a folder in a non-existing parent folder", vcr: "one_drive/create_folder_parent_not_found" do + let(:folder_name) { "Földer CreatedBy Çommand" } + let(:parent_location) { "01AZJL5PKU2WV3U3RKKFF4A7ZCWVBXRTEU" } + + it_behaves_like "adapter create_folder_command: parent not found" + end + + context "when folder already exists", vcr: "one_drive/create_folder_already_exists" do + let(:folder_name) { "Folder" } + let(:parent_location) { "/" } + + it_behaves_like "adapter create_folder_command: folder already exists" + end + + private + + def delete_created_folder(folder) + Input::DeleteFolder.build(location: folder.id).bind do |input_data| + Registry.resolve("one_drive.commands.delete_folder").call(storage:, auth_strategy:, input_data:) + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/delete_folder_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/delete_folder_command_spec.rb new file mode 100644 index 00000000000..d0a8fb243ea --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/delete_folder_command_spec.rb @@ -0,0 +1,80 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Commands + RSpec.describe DeleteFolderCommand, :vcr, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage) } + let(:auth_strategy) { Registry["one_drive.authentication.userless"].call } + + it "is registered as commands.one_drive.delete_folder" do + expect(Registry.resolve("one_drive.commands.delete_folder")).to eq(described_class) + end + + it ".call requires storage and location as keyword arguments" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) + end + + it "deletes a folder", vcr: "one_drive/delete_folder" do + create_result = Input::CreateFolder + .build(folder_name: "To Be Deleted Soon", parent_location: "/").bind do |input_data| + Registry.resolve("one_drive.commands.create_folder").call(storage:, auth_strategy:, input_data:) + end + + folder = create_result.value_or { fail("Folder Creation Failed") } + + Input::DeleteFolder.build(location: folder.id).bind do |input_data| + expect(described_class.call(storage:, auth_strategy:, input_data:)).to be_success + end + end + + it "when the folder is not found, returns a failure", vcr: "one_drive/delete_folder_not_found" do + result = Input::DeleteFolder.build(location: "NOT_HERE").bind do |input_data| + described_class.call(storage:, auth_strategy:, input_data:) + end + + expect(result).to be_failure + expect(result.failure.code).to eq(:not_found) + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/rename_file_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/rename_file_command_spec.rb new file mode 100644 index 00000000000..b14dcf9ea6f --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/rename_file_command_spec.rb @@ -0,0 +1,72 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Commands + RSpec.describe RenameFileCommand, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage) } + let(:auth_strategy) { Registry.resolve("one_drive.authentication.userless").call } + let(:input_data) { Input::RenameFile.build(location: file_id, new_name: name).value! } + + it_behaves_like "adapter rename_file_command: basic command setup" + + context "when renaming a folder", vcr: "one_drive/rename_file_success" do + let(:file_id) { "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR" } + let(:name) { "I am the senat" } + + it_behaves_like "adapter rename_file_command: successful file renaming" + end + + context "when renaming a file inside a subdirectory", vcr: "one_drive/rename_file_with_location_success" do + let(:file_id) { "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN" } + let(:name) { "I❤️you death star.png" } + + it_behaves_like "adapter rename_file_command: successful file renaming" + end + + context "when trying to rename a not existent file", vcr: "one_drive/rename_file_not_found" do + let(:file_id) { "sith_have_yellow_light_sabers" } + let(:name) { "this_will_not_happen.png" } + let(:error_source) { described_class } + + it_behaves_like "adapter rename_file_command: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/set_permissions_command_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/set_permissions_command_spec.rb new file mode 100644 index 00000000000..2916b92b0ec --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/commands/set_permissions_command_spec.rb @@ -0,0 +1,180 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Commands + RSpec.describe SetPermissionsCommand, :webmock do + let(:storage) do + create(:sharepoint_dev_drive_storage, + drive_id: "b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2ODRDvn3haLiQIhB5UYNdqMy") + end + + let(:auth_strategy) { Adapters::Registry.resolve("one_drive.authentication.userless")[false] } + let(:test_folder_data) do + Input::CreateFolder.build(folder_name: "Permission Test Folder", parent_location: "/").value! + end + + let(:test_folder) do + Registry.resolve("one_drive.commands.create_folder") + .call(storage:, auth_strategy:, input_data: test_folder_data).value! + end + + it_behaves_like "adapter set_permissions_command: basic command setup" + + context "if folder does not exists", vcr: "one_drive/set_permissions_not_found_folder" do + let(:error_source) { described_class } + let(:input_data) { permission_input_data("THIS_IS_NOT_THE_FOLDER_YOURE_LOOKING_FOR", []) } + + it_behaves_like "adapter set_permissions_command: not found" + end + + context "if a write roles is already set" do + def current_remote_permissions + permission_list_from_role("write") + end + + context "and new write permissions should be set", vcr: "one_drive/set_permissions_replace_permissions_write" do + let(:previous_permissions) { [{ user_id: "84acc1d5-61be-470b-9d79-0d1f105c2c5f", permissions: [:write_files] }] } + let(:replacing_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:write_files] }] } + + it_behaves_like "adapter set_permissions_command: replaces already set permissions" + end + + context "and they should get deleted", vcr: "one_drive/set_permissions_delete_permission_write" do + let(:previous_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:write_files] }] } + let(:replacing_permissions) { [] } + + it_behaves_like "adapter set_permissions_command: replaces already set permissions" + end + end + + context "if a read roles is already set", vcr: "one_drive/set_permissions_replace_permissions_read" do + def current_remote_permissions + permission_list_from_role("read") + end + + context "and new read permissions should be set", vcr: "one_drive/set_permissions_replace_permissions_read" do + let(:previous_permissions) { [{ user_id: "84acc1d5-61be-470b-9d79-0d1f105c2c5f", permissions: [:read_files] }] } + let(:replacing_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files] }] } + + it_behaves_like "adapter set_permissions_command: replaces already set permissions" + end + + context "and they should get deleted", vcr: "one_drive/set_permissions_delete_permission_read" do + let(:previous_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files] }] } + let(:replacing_permissions) { [] } + + it_behaves_like "adapter set_permissions_command: replaces already set permissions" + end + end + + context "if no write permission exists", vcr: "one_drive/set_permissions_create_permission_write" do + let(:user_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:write_files] }] } + + def current_remote_permissions + permission_list_from_role("write") + end + + it_behaves_like "adapter set_permissions_command: creates new permissions" + end + + context "if no read permission exists", vcr: "one_drive/set_permissions_create_permission_read" do + let(:user_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files] }] } + + def current_remote_permissions + permission_list_from_role("read") + end + + it_behaves_like "adapter set_permissions_command: creates new permissions" + end + + context "if a timeout occurs" do + it "logs an error", vcr: "one_drive/set_permissions_delete_permission_read" do + stub_request_with_timeout(:post, /invite$/) + allow(Rails.logger).to receive(:error) + + user_permissions = [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", + permissions: [:read_files] }] + input_data = permission_input_data(test_folder.id, user_permissions) + described_class.call(storage:, auth_strategy:, input_data:) + + expect(Rails.logger).to have_received(:error).with( + error_code: :error, + data: %r{timed out while waiting on select \(HTTPX::ConnectTimeoutError\)\n$} + ).once + end + end + + private + + def permission_input_data(file_id, user_permissions) + Input::SetPermissions.build(file_id:, user_permissions:).value! + end + + def clean_up(file_id) + Input::DeleteFolder.build(location: file_id).bind do |input_data| + Registry.resolve("one_drive.commands.delete_folder").call(storage:, auth_strategy:, input_data:) + end + end + + def permission_list_from_role(role) + perm = role == "write" ? :write_files : :read_files + + remote_permissions + .select { |item| item[:roles].first == role } + .map { |grant| grant.dig(:grantedToV2, :user, :id) } + .map { |id| { user_id: id, permissions: [perm] } } + end + + def remote_permissions + Authentication[auth_strategy].call(storage:) do |http| + http.get(UrlBuilder.url(storage.uri, + "/v1.0/drives", + storage.drive_id, + "/items", + test_folder.id, + "/permissions")) + .raise_for_status + .json(symbolize_keys: true) + .fetch(:value) + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/one_drive_contract_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/one_drive_contract_spec.rb new file mode 100644 index 00000000000..d9d49cf0555 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/one_drive_contract_spec.rb @@ -0,0 +1,89 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + RSpec.describe OneDriveContract, :storage_server_helpers, :webmock do + let(:current_user) { create(:admin) } + let(:storage) { build(:one_drive_storage) } + + # As the OneDriveContract is selected by the BaseContract to make writable attributes available, + # the BaseContract needs to be instantiated here. + subject(:contract) { Storages::BaseContract.new(storage, current_user) } + + describe "when a host is set" do + before do + storage.host = "https://exmaple.com/" + end + + it "must be invalid" do + expect(contract).not_to be_valid + end + end + + context "with tenant that is no UUID" do + let(:storage) { build(:one_drive_storage, tenant_id: "123") } + + it "is invalid" do + expect(contract).not_to be_valid + + expect(contract.errors[:tenant_id]).to eq(["is invalid."]) + end + end + + context "with blank Drive ID" do + let(:storage) { build(:one_drive_storage, drive_id: "") } + + it "is invalid" do + expect(contract).not_to be_valid + + expect(contract.errors[:drive_id]).to eq(["can't be blank.", "is too short (minimum is 17 characters)."]) + end + end + + context "with short Drive ID" do + let(:storage) { build(:one_drive_storage, drive_id: "1234567890") } + + it "is invalid" do + expect(contract).not_to be_valid + + expect(contract.errors[:drive_id]).to eq(["is too short (minimum is 17 characters)."]) + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/download_link_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/download_link_query_spec.rb new file mode 100644 index 00000000000..36124cb6387 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/download_link_query_spec.rb @@ -0,0 +1,87 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Queries + RSpec.describe DownloadLinkQuery, :vcr, :webmock do + let(:user) { create(:user) } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + let(:auth_strategy) { Registry["one_drive.authentication.user_bound"].call(user, storage) } + + let(:file_link) { create(:file_link, origin_id: "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE") } + let(:not_existent_file_link) { create(:file_link, origin_id: "DeathStarNumberThree") } + + let(:input_data) { Input::DownloadLink.build(file_link:).value! } + + subject { described_class.new(storage) } + + describe "#call" do + it "responds with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) + end + + context "with outbound request successful" do + it "returns a result with a download url", vcr: "one_drive/download_link_query_success" do + download_link = subject.call(auth_strategy:, input_data:) + + expect(download_link).to be_success + + uri = download_link.value! + expect(uri.host).to eq("finn.sharepoint.com") + expect(uri.path).to eq("/sites/openprojectfilestoragetests/_layouts/15/download.aspx") + end + + it "returns an error if the file is not found", vcr: "one_drive/download_link_query_not_found" do + input_data = Input::DownloadLink.build(file_link: not_existent_file_link).value! + + download_link = subject.call(auth_strategy:, input_data:) + expect(download_link).to be_failure + + error = download_link.failure + expect(error.source).to eq(described_class) + expect(error.code).to eq(:not_found) + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/file_info_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/file_info_query_spec.rb new file mode 100644 index 00000000000..62d047a49ff --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/file_info_query_spec.rb @@ -0,0 +1,134 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Queries + RSpec.describe FileInfoQuery, :webmock do + let(:user) { create(:user) } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + + let(:auth_strategy) { Registry["one_drive.authentication.user_bound"].call(user, storage) } + + let(:input_data) { Input::FileInfo.build(file_id:).value! } + + it_behaves_like "adapter file_info_query: basic query setup" + + context "with a file id requested", vcr: "one_drive/file_info_query_success_file" do + let(:file_id) { "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA" } + let(:file_info) do + Results::StorageFileInfo.new( + id: file_id, + status: "ok", + status_code: 200, + name: "NextcloudHub.md", + size: 1095, + mime_type: "application/octet-stream", + created_at: Time.parse("2023-09-26T14:45:25Z"), + last_modified_at: Time.parse("2023-09-26T14:46:13Z"), + owner_name: "Eric Schubert", + owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + last_modified_by_name: "Eric Schubert", + last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + permissions: nil, + location: "/Folder/Subfolder/NextcloudHub.md" + ) + end + + it_behaves_like "adapter file_info_query: successful file/folder response" + end + + context "with a folder id requested", vcr: "one_drive/file_info_query_success_folder" do + let(:file_id) { "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB" } + let(:file_info) do + Results::StorageFileInfo.new( + id: file_id, + status: "ok", + status_code: 200, + name: "Ümlæûts", + size: 20789, + mime_type: "application/x-op-directory", + created_at: Time.parse("2023-10-09T15:26:32Z"), + last_modified_at: Time.parse("2023-10-09T15:26:32Z"), + owner_name: "Eric Schubert", + owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + last_modified_by_name: "Eric Schubert", + last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + permissions: nil, + location: "/Folder/%C3%9Cml%C3%A6%C3%BBts" + ) + end + + it_behaves_like "adapter file_info_query: successful file/folder response" + end + + context "with a file with special characters in the path", + vcr: "one_drive/file_info_query_success_special_characters" do + let(:file_id) { "01AZJL5PITB4FWUTEDCZGLV3WXG5TJX5A2" } + let(:file_info) do + Results::StorageFileInfo.new( + id: file_id, + status: "ok", + status_code: 200, + name: "what_have_you_done.png", + size: 226985, + mime_type: "image/png", + created_at: Time.parse("2024-06-17T09:37:58Z"), + last_modified_at: Time.parse("2024-06-17T09:38:15Z"), + owner_name: "Eric Schubert", + owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + last_modified_by_name: "Eric Schubert", + last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + permissions: nil, + location: + "/Folder%20with%20spaces/%C3%9Cml%C3%A4uts%20%26%20spe%C2%A2i%C3%A6l%20characters/what_have_you_done.png" + ) + end + + it_behaves_like "adapter file_info_query: successful file/folder response" + end + + context "with a not existing file id", vcr: "one_drive/file_info_query_not_found" do + let(:file_id) { "not_existent" } + let(:error_source) { Internal::DriveItemQuery } + + it_behaves_like "adapter file_info_query: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/file_path_to_id_map_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/file_path_to_id_map_query_spec.rb new file mode 100644 index 00000000000..9945760c8bb --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/file_path_to_id_map_query_spec.rb @@ -0,0 +1,131 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Queries + RSpec.describe FilePathToIdMapQuery, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage) } + let(:auth_strategy) { Adapters::Registry["one_drive.authentication.userless"].call } + let(:depth) { Float::INFINITY } + let(:input_data) { Input::FilePathToIdMap.build(folder:, depth:).value! } + + it_behaves_like "adapter file_path_to_id_map_query: basic query setup" + + context "with parent folder being root", vcr: "one_drive/file_path_to_id_map_query_root" do + let(:folder) { "/" } + + context "with unset depth (defaults to INFINITY)" do + let(:expected_ids) do + { + "/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", + "/Folder with spaces" => "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", + "/Folder with spaces/very empty folder" => "01AZJL5PMGEIRPHZPHRRH2NM3D734VIR7H", + "/Folder with spaces/wordle1.png" => "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN", + "/Folder with spaces/wordle2.png" => "01AZJL5PIIFUD6A765KBAIAEMYACAFB2WP", + "/Folder with spaces/wordle3.png" => "01AZJL5PL4AUJEU43CQZFJKN7BQPRP3BLF", + "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", + "/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI", + "/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", + "/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", + "/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", + "/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS", + "/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY", + "/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46", + "/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", + "/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", + "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE", + "/Permissions Folder" => "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB" + } + end + + it_behaves_like "adapter file_path_to_id_map_query: successful query" + end + + context "with a depth of 0" do + let(:depth) { 0 } + let(:expected_ids) { { "/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ" } } + + it_behaves_like "adapter file_path_to_id_map_query: successful query" + end + + context "with a depth of 1" do + let(:depth) { 1 } + let(:expected_ids) do + { + "/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", + "/Folder with spaces" => "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", + "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", + "/Permissions Folder" => "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB" + } + end + + it_behaves_like "adapter file_path_to_id_map_query: successful query" + end + end + + context "with a given parent folder", vcr: "one_drive/file_path_to_id_map_query_parent_folder" do + let(:folder) { "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR" } + let(:expected_ids) do + { + "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", + "/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI", + "/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", + "/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", + "/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", + "/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS", + "/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY", + "/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46", + "/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", + "/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", + "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE" + } + end + + it_behaves_like "adapter file_path_to_id_map_query: successful query" + end + + context "with not existent parent folder", vcr: "one_drive/file_path_to_id_map_query_invalid_parent" do + let(:folder) { "/I/just/made/that/up" } + let(:error_source) { Internal::DriveItemQuery } + + it_behaves_like "adapter file_path_to_id_map_query: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/files_info_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/files_info_query_spec.rb new file mode 100644 index 00000000000..f53abd19bbb --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/files_info_query_spec.rb @@ -0,0 +1,163 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Queries + RSpec.describe FilesInfoQuery, :vcr, :webmock do + let(:user) { create(:user) } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + let(:auth_strategy) { Registry["one_drive.authentication.user_bound"].call(user, storage) } + let(:input_data) { Input::FilesInfo.build(file_ids:).value! } + + subject(:query) { described_class.new(storage) } + + describe "#call" do + it "responds with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) + end + + context "without outbound request involved" do + context "with an empty array of file ids" do + let(:file_ids) { [] } + + it "returns an empty array" do + result = query.call(auth_strategy:, input_data:) + + expect(result).to be_success + expect(result.value!).to eq([]) + end + end + end + + context "with outbound requests successful", vcr: "one_drive/files_info_query_success" do + context "with an array of file ids" do + let(:file_ids) do + %w( + 01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU + 01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU + 01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA + ) + end + + # rubocop:disable RSpec/ExampleLength + it "must return an array of file information when called" do + result = query.call(auth_strategy:, input_data:) + expect(result).to be_success + + file_infos = result.value! + expect(file_infos.size).to eq(3) + expect(file_infos).to all(be_a(Results::StorageFileInfo)) + expect(file_infos.map(&:to_h)) + .to eq([{ + status: "ok", + status_code: 200, + id: "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", + name: "Folder with spaces", + size: 35141, + mime_type: "application/x-op-directory", + created_at: Time.parse("2023-09-26T14:38:57Z"), + last_modified_at: Time.parse("2023-09-26T14:38:57Z"), + owner_name: "Eric Schubert", + owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + last_modified_by_name: "Eric Schubert", + last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + permissions: nil, + location: "/Folder%20with%20spaces" + }, + { + status: "ok", + status_code: 200, + id: "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", + name: "Document.docx", + size: 22514, + mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + created_at: Time.parse("2023-09-26T14:40:58Z"), + last_modified_at: Time.parse("2023-09-26T14:42:03Z"), + owner_name: "Eric Schubert", + owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + last_modified_by_name: "Eric Schubert", + last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + permissions: nil, + location: "/Folder/Document.docx" + }, + { + status: "ok", + status_code: 200, + id: "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", + name: "NextcloudHub.md", + size: 1095, + mime_type: "application/octet-stream", + created_at: Time.parse("2023-09-26T14:45:25Z"), + last_modified_at: Time.parse("2023-09-26T14:46:13Z"), + owner_name: "Eric Schubert", + owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + last_modified_by_name: "Eric Schubert", + last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", + permissions: nil, + location: "/Folder/Subfolder/NextcloudHub.md" + }]) + end + # rubocop:enable RSpec/ExampleLength + end + end + + context "with one outbound request returning not found", vcr: "one_drive/files_info_query_one_not_found" do + context "with an array of file ids" do + let(:file_ids) { %w[01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU not_existent] } + + it "must return an array of file information when called" do + result = query.call(auth_strategy:, input_data:) + expect(result).to be_success + file_infos = result.value! + + expect(file_infos.size).to eq(2) + expect(file_infos).to all(be_a(Results::StorageFileInfo)) + expect(file_infos[1].id).to eq("not_existent") + expect(file_infos[1].status).to eq(:not_found) + expect(file_infos[1].status_code).to eq(404) + end + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/files_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/files_query_spec.rb new file mode 100644 index 00000000000..ca4039e7f66 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/files_query_spec.rb @@ -0,0 +1,211 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Queries + RSpec.describe FilesQuery, :webmock do + let(:user) { create(:user) } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + let(:auth_strategy) { Registry["one_drive.authentication.user_bound"].call(user, storage) } + let(:input_data) { Input::Files.build(folder:).value! } + + it_behaves_like "adapter files_query: basic query setup" + + context "with parent folder being root", vcr: "one_drive/files_query_root" do + let(:folder) { "/" } + let(:files_result) do + Results::StorageFileCollection.build( + files: [ + Results::StorageFile.new(id: "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", + name: "Folder", + size: 260500, + mime_type: "application/x-op-directory", + created_at: Time.zone.parse("2023-09-26T14:38:50Z"), + last_modified_at: Time.zone.parse("2023-09-26T14:38:50Z"), + created_by_name: "Eric Schubert", + last_modified_by_name: "Eric Schubert", + location: "/Folder", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", + name: "Folder with spaces", + size: 35141, + mime_type: "application/x-op-directory", + created_at: Time.zone.parse("2023-09-26T14:38:57Z"), + last_modified_at: Time.zone.parse("2023-09-26T14:38:57Z"), + created_by_name: "Eric Schubert", + last_modified_by_name: "Eric Schubert", + location: "/Folder%20with%20spaces", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB", + name: "Permissions Folder", + size: 0, + mime_type: "application/x-op-directory", + created_at: Time.zone.parse("2024-01-12T09:05:10Z"), + last_modified_at: Time.zone.parse("2024-01-12T09:05:24Z"), + created_by_name: "Marcello Rocha", + last_modified_by_name: "Marcello Rocha", + location: "/Permissions%20Folder", + permissions: %i[readable writeable]) + ], + parent: Results::StorageFile.new(id: "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", + name: "Root", + location: "/", + permissions: %i[readable writeable]), + ancestors: [] + ).value! + end + + it_behaves_like "adapter files_query: successful files response" + end + + context "with a given parent folder", vcr: "one_drive/files_query_parent_folder" do + let(:folder) { "/Folder/Subfolder" } + let(:files_result) do + Results::StorageFileCollection.build( + files: [ + Results::StorageFile.new(id: "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", + name: "NextcloudHub.md", + size: 1095, + mime_type: "application/octet-stream", + created_at: Time.zone.parse("2023-09-26T14:45:25Z"), + last_modified_at: Time.zone.parse("2023-09-26T14:46:13Z"), + created_by_name: "Eric Schubert", + last_modified_by_name: "Eric Schubert", + location: "/Folder/Subfolder/NextcloudHub.md", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", + name: "test.txt", + size: 28, + mime_type: "text/plain", + created_at: Time.zone.parse("2023-09-26T14:45:23Z"), + last_modified_at: Time.zone.parse("2023-09-26T14:45:45Z"), + created_by_name: "Eric Schubert", + last_modified_by_name: "Eric Schubert", + location: "/Folder/Subfolder/test.txt", + permissions: %i[readable writeable]) + ], + parent: Results::StorageFile.new(id: "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", + name: "Subfolder", + location: "/Folder/Subfolder", + permissions: %i[readable writeable]), + ancestors: [ + Results::StorageFile.new(id: "a1d45ff742d2175c095f0a7173f93fc3fc23664a953ceae6778fe15398818c2d", + name: "Root", + location: "/", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "74ccd43303847f2655300641a934959cdb11689ce171aa0f00faa92917fbd340", + name: "Folder", + location: "/Folder") + ] + ).value! + end + + it_behaves_like "adapter files_query: successful files response" + end + + context "with parent folder being empty", vcr: "one_drive/files_query_empty_folder" do + let(:folder) { "/Folder with spaces/very empty folder" } + let(:files_result) do + Results::StorageFileCollection.build( + files: [], + parent: Results::StorageFile.new(id: "01AZJL5PMGEIRPHZPHRRH2NM3D734VIR7H", + name: "very empty folder", + location: "/Folder%20with%20spaces/very%20empty%20folder", + permissions: %i[readable writeable]), + ancestors: [ + Results::StorageFile.new(id: "a1d45ff742d2175c095f0a7173f93fc3fc23664a953ceae6778fe15398818c2d", + name: "Root", + location: "/", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "58bde0c7931c8f95bb1bf525471146090630cb72827cb1e63dcaab3a9adce763", + name: "Folder with spaces", + location: "/Folder%20with%20spaces") + ] + ).value! + end + + it_behaves_like "adapter files_query: successful files response" + end + + context "with a path full of umlauts", vcr: "one_drive/files_query_umlauts" do + let(:folder) { "/Folder/Ümlæûts" } + let(:files_result) do + Results::StorageFileCollection.build( + files: [ + Results::StorageFile.new( + id: "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE", + name: "Anrüchiges deutsches Dokument.docx", + size: 18007, + mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + created_at: Time.zone.parse("2023-10-09T15:26:45Z"), + last_modified_at: Time.zone.parse("2023-10-09T15:27:25Z"), + created_by_name: "Eric Schubert", + last_modified_by_name: "Eric Schubert", + location: "/Folder/%C3%9Cml%C3%A6%C3%BBts/Anr%C3%BCchiges%20deutsches%20Dokument.docx", + permissions: %i[readable writeable] + ) + ], + parent: Results::StorageFile.new(id: "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", + name: "Ümlæûts", + location: "/Folder/%C3%9Cml%C3%A6%C3%BBts", + permissions: %i[readable writeable]), + ancestors: [ + Results::StorageFile.new(id: "a1d45ff742d2175c095f0a7173f93fc3fc23664a953ceae6778fe15398818c2d", + name: "Root", + location: "/", + permissions: %i[readable writeable]), + Results::StorageFile.new(id: "74ccd43303847f2655300641a934959cdb11689ce171aa0f00faa92917fbd340", + name: "Folder", + location: "/Folder") + ] + ).value! + end + + it_behaves_like "adapter files_query: successful files response" + end + + context "with not existent parent folder", vcr: "one_drive/files_query_invalid_parent" do + let(:folder) { "/I/just/made/that/up" } + let(:error_source) { described_class } + + it_behaves_like "adapter files_query: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/open_file_link_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/open_file_link_query_spec.rb new file mode 100644 index 00000000000..b0d1c035711 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/open_file_link_query_spec.rb @@ -0,0 +1,92 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Queries + RSpec.describe OpenFileLinkQuery, :vcr, :webmock do + let(:user) { create(:user) } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + let(:file_id) { "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU" } + let(:auth_strategy) { Registry.resolve("one_drive.authentication.user_bound").call(user, storage) } + let(:input_data) { Input::OpenFileLink.build(file_id:).value! } + + subject { described_class.new(storage) } + + describe "#call" do + it "responds with correct parameters" do + expect(described_class).to respond_to(:call) + + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], + %i[keyreq auth_strategy], + %i[keyreq input_data]) + end + + context "with outbound requests successful" do + context "with open location flag not set", vcr: "one_drive/open_file_link_query_success" do + it "returns the url for opening the file on storage" do + call = subject.call(auth_strategy:, input_data:) + expect(call).to be_success + expect(call.value!).to eq("https://finn.sharepoint.com/sites/openprojectfilestoragetests/_layouts/15/Doc.aspx?sourcedoc=%7B3D884033-B88B-4195-8F36-D30B41AB9234%7D&file=Document.docx&action=default&mobileredirect=true") + end + end + + context "with open location flag set", vcr: "one_drive/open_file_link_location_query_success" do + it "returns the url for opening the file on storage" do + call = subject.call(auth_strategy:, input_data: input_data.with(open_location: true)) + expect(call).to be_success + expect(call.value!).to eq("https://finn.sharepoint.com/sites/openprojectfilestoragetests/VCR/Folder") + end + end + end + + context "with not existent file id", vcr: "one_drive/open_file_link_query_missing_file_id" do + let(:file_id) { "iamnotexistent" } + + it "must return not found" do + result = subject.call(auth_strategy:, input_data:) + expect(result).to be_failure + expect(result.failure.source).to be(Internal::DriveItemQuery) + expect(result.failure.code).to eq(:not_found) + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/authentication_validator_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/open_storage_query_spec.rb similarity index 56% rename from modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/authentication_validator_spec.rb rename to modules/storages/spec/common/storages/adapters/providers/one_drive/queries/open_storage_query_spec.rb index b8018b90460..42a64cbb187 100644 --- a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/authentication_validator_spec.rb +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/open_storage_query_spec.rb @@ -32,37 +32,34 @@ require "spec_helper" require_module_spec_helper module Storages - module Peripherals - module ConnectionValidators + module Adapters + module Providers module OneDrive - RSpec.describe AuthenticationValidator, :webmock do - subject(:validator) { described_class.new(storage) } - - context "when using OAuth2" do + module Queries + RSpec.describe OpenStorageQuery, :webmock do let(:user) { create(:user) } let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - - before { User.current = user } - - it "passes when the user has a token and the request works", vcr: "one_drive/user_query_success" do - expect(validator.call).to be_success + let(:auth_strategy) do + Registry["one_drive.authentication.user_bound"].call(user, storage) end - it "returns a warning when there's no token for the current user" do - User.current = create(:user) - result = validator.call + subject { described_class.new(storage) } - expect(result[:existing_token]).to be_a_warning - expect(result[:existing_token].code).to eq(:od_oauth_token_missing) - expect(result[:user_bound_request]).to be_skipped - end + describe "#call" do + it "responds with correct parameters" do + expect(described_class).to respond_to(:call) - it "returns a failure if the remote call failed" do - Registry.stub("one_drive.queries.user", ->(_) { ServiceResult.failure(result: :unauthorized) }) + method = described_class.method(:call) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) + end - result = validator.call - expect(result[:user_bound_request]).to be_a_failure - expect(result[:user_bound_request].code).to eq(:od_oauth_request_unauthorized) + context "with outbound requests successful", vcr: "one_drive/open_storage_query_success" do + it "returns the url for opening the storage" do + call = subject.call(auth_strategy:) + expect(call).to be_success + expect(call.value!).to eq("https://finn.sharepoint.com/sites/openprojectfilestoragetests/VCR") + end + end end end end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/upload_link_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/upload_link_query_spec.rb new file mode 100644 index 00000000000..5aee85c5f15 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/upload_link_query_spec.rb @@ -0,0 +1,87 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Queries + RSpec.describe UploadLinkQuery, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage) } + let(:auth_strategy) { Registry["one_drive.authentication.userless"].call } + + it_behaves_like "adapter upload_link_query: basic query setup" + + context "when requesting an upload link for an existing file", vcr: "one_drive/upload_link_success" do + let(:input_data) do + Input::UploadLink + .build(folder_id: "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", file_name: "DeathStart_blueprints.tiff").value! + end + + let(:token) do + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBfZGlzcGxheW5hbWUiOiJPcGVuUHJvamVjdCBEZXYgQXBwIiwiYXVkIjoiMDAwMDA" \ + "wMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL2Zpbm4uc2hhcmVwb2ludC5jb21ANGQ0NGJmMzYtOWI1Ni00NWMwLTg4MDctYmJmMzg" \ + "2ZGQwNDdmIiwiY2lkIjoiR3k0SDY0aTF2MEN6NXVxU0tDTkNodz09IiwiZW5kcG9pbnR1cmwiOiJ6cFdrZGttVmxSUEZYRG55eWVmb0thaUg" \ + "ycFhmV0RUdmkvNTVReHVYSlAwPSIsImVuZHBvaW50dXJsTGVuZ3RoIjoiMjc3IiwiZXhwIjoiMTcxNTA5MjYxOCIsImlwYWRkciI6IjIwLjE" \ + "5MC4xOTAuMTAwIiwiaXNsb29wYmFjayI6IlRydWUiLCJpc3MiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAiLCJuYW1" \ + "laWQiOiI0MjYyZGYyYi03N2JiLTQ5YzItYTVkZi0yODM1NWRhNjc2ZDJANGQ0NGJmMzYtOWI1Ni00NWMwLTg4MDctYmJmMzg2ZGQwNDdmIiw" \ + "ibmJmIjoiMTcxNTAwNjIxOCIsInJvbGVzIjoiYWxsc2l0ZXMucmVhZCBhbGxzaXRlcy53cml0ZSBhbGxmaWxlcy53cml0ZSIsInNpdGVpZCI" \ + "6Ik1XSTBZalkxTnpZdE9UQTJaQzAwWkRrMExUaG1ORGt0Tm1Rd01HRTVOVEEzWWpVdyIsInR0IjoiMSIsInZlciI6Imhhc2hlZHByb29mdG9" \ + "rZW4ifQ.UMqPAjuiXSt1rQgFiE0h-k3wkBZ3DmF3I3Nj_zYuYuI" + end + + let(:upload_url) do + "https://finn.sharepoint.com/sites/openprojectfilestoragetests/_api/v2.0/drives/" \ + "b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01AZJL5PKRK4XUJQQH3JHIUGK2ALGEJEK4/" \ + "uploadSession?guid=%2789f10eb4-b8d9-4ba9-ab64-eb1e6d39b2ee%27&overwrite=False&rename=True&dc=0" \ + "&tempauth=#{token}" + end + let(:upload_method) { :put } + + it_behaves_like "adapter upload_link_query: successful upload link response" + end + + context "when requesting an upload link for a not existing file", vcr: "one_drive/upload_link_not_found" do + let(:input_data) do + Input::UploadLink + .build(folder_id: "04AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", file_name: "DeathStart_blueprints.tiff").value! + end + + it_behaves_like "adapter upload_link_query: not found" + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/user_query_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/user_query_spec.rb new file mode 100644 index 00000000000..a56457fcd3a --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/queries/user_query_spec.rb @@ -0,0 +1,86 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Queries + RSpec.describe UserQuery, :webmock do + let(:user) { create(:user) } + + let(:storage) do + create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) + end + + let(:user_bound_strategy) do + Registry.resolve("one_drive.authentication.user_bound").call(user, storage) + end + + let(:auth_strategy) { user_bound_strategy } + + it "is registered" do + expect(Registry.resolve("one_drive.queries.user")).to eq(described_class) + end + + it "responds to .call" do + expect(described_class).to respond_to(:call) + end + + it ".call takes required parameters" do + method = described_class.method(:call) + + expect(method.parameters).to contain_exactly(%i[keyreq auth_strategy], %i[keyreq storage]) + end + + it "responds with user details if request is successful", vcr: "one_drive/user_query_success" do + command_result = described_class.call(auth_strategy:, storage:) + + expect(command_result).to be_success + expect(command_result.value!).to eq(id: "a9023fd0-c421-4695-b83c-bb3ba67708d6") + end + + it "responds with unauthorized if request is unauthorized", vcr: "one_drive/user_query_unauthorized" do + command_result = described_class.call(auth_strategy:, storage:) + + expect(command_result).to be_failure + + error = command_result.failure + expect(error.code).to eq(:unauthorized) + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/peripherals/one_drive_storage_wizard_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/storage_wizard_spec.rb similarity index 98% rename from modules/storages/spec/common/storages/peripherals/one_drive_storage_wizard_spec.rb rename to modules/storages/spec/common/storages/adapters/providers/one_drive/storage_wizard_spec.rb index d06fcd07847..45cc9162718 100644 --- a/modules/storages/spec/common/storages/peripherals/one_drive_storage_wizard_spec.rb +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/storage_wizard_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" -RSpec.describe Storages::Peripherals::OneDriveStorageWizard do +RSpec.describe Storages::Adapters::Providers::OneDrive::StorageWizard do subject(:wizard) { described_class.new(model:, user:) } let(:model) { Storages::OneDriveStorage.new } diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/ampf_configuration_validator_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/ampf_configuration_validator_spec.rb new file mode 100644 index 00000000000..94f61ce6181 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/ampf_configuration_validator_spec.rb @@ -0,0 +1,124 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Validators + RSpec.describe AmpfConfigurationValidator, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage, :as_automatically_managed) } + let(:auth_strategy) { Registry["one_drive.authentication.userless"].call } + let(:folder_name) { described_class::TEST_FOLDER_NAME } + + subject(:validator) { described_class.new(storage) } + + it "returns a GroupValidationResult", vcr: "one_drive/validator_ampf_clean_run" do + results = validator.call + + expect(results).to be_a(ConnectionValidators::ValidationGroupResult) + expect(results).to be_success + end + + describe "possible error scenarios" do + it "fails when there's unexpected folder and files in the drive", vcr: "one_drive/validator_extraneous_files" do + results = validator.call + + expect(results[:drive_contents]).to be_a_warning + expect(results[:drive_contents].code).to eq(:od_unexpected_content) + end + + it "fails when folders can't be created" do + create_cmd = class_double(Commands::CreateFolderCommand) + input_data = Input::CreateFolder.build(folder_name:, parent_location: "/").value! + error = Results::Error.new(source: self, code: :error) + allow(create_cmd).to receive(:call).with(storage:, auth_strategy:, input_data:).and_return(Failure(error)) + + Registry.stub("one_drive.commands.create_folder", create_cmd) + + results = validator.call + + expect(results[:client_folder_creation]).to be_a_failure + expect(results[:client_folder_creation].code).to eq(:od_client_write_permission_missing) + end + + it "fails when the test folder already exists on the remote", + vcr: "one_drive/validator_test_folder_already_exists" do + Input::CreateFolder.build(folder_name:, parent_location: "/").bind do |input_data| + Registry["one_drive.commands.create_folder"].call(storage:, auth_strategy:, input_data:) + end + + result = validator.call + expect(result[:client_folder_creation]).to be_a_failure + expect(result[:client_folder_creation].code).to eq(:od_existing_test_folder) + expect(result[:client_folder_creation].context[:folder_name]).to eq(folder_name) + ensure + Input::DeleteFolder.build(location: created_folder).bind do |input_data| + Commands::DeleteFolderCommand.call(storage:, auth_strategy:, input_data:) + end + end + + it "fails when folders can't be deleted", vcr: "one_drive/validator_create_folder" do + delete_cmd = class_double(Commands::DeleteFolderCommand) + allow(delete_cmd).to receive(:call).and_return(Failure()) + + Registry.stub("one_drive.commands.delete_folder", delete_cmd) + + results = validator.call + + expect(results[:client_folder_removal]).to be_a_failure + expect(results[:client_folder_removal].code).to eq(:od_client_cant_delete_folder) + ensure + Input::DeleteFolder.build(location: created_folder).bind do |input_data| + Commands::DeleteFolderCommand.call(storage:, auth_strategy:, input_data:) + end + end + end + + private + + def created_folder + Input::Files.build(folder: "/").bind do |input_data| + Registry["one_drive.queries.files"].call(storage:, auth_strategy:, input_data:).bind do |result| + folder = result.all_folders.detect { |file| file.name.include?(folder_name) } + + return folder.id + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/authentication_validator_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/authentication_validator_spec.rb new file mode 100644 index 00000000000..e0b7a8e033e --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/authentication_validator_spec.rb @@ -0,0 +1,75 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Validators + RSpec.describe AuthenticationValidator, :webmock do + subject(:validator) { described_class.new(storage) } + + context "when using OAuth2" do + let(:user) { create(:user) } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + let(:error) { Results::Error.new(code: :unauthorized, source: self) } + + before { User.current = user } + + it "passes when the user has a token and the request works", vcr: "one_drive/user_query_success" do + expect(validator.call).to be_success + end + + it "returns a warning when there's no token for the current user" do + User.current = create(:user) + result = validator.call + + expect(result[:existing_token]).to be_a_warning + expect(result[:existing_token].code).to eq(:od_oauth_token_missing) + expect(result[:user_bound_request]).to be_skipped + end + + it "returns a failure if the remote call failed" do + Registry.stub("one_drive.queries.user", ->(_) { Failure(error) }) + + result = validator.call + expect(result[:user_bound_request]).to be_a_failure + expect(result[:user_bound_request].code).to eq(:od_oauth_request_unauthorized) + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/storage_configuration_validator_spec.rb b/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/storage_configuration_validator_spec.rb new file mode 100644 index 00000000000..d59be9a6e93 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/providers/one_drive/validators/storage_configuration_validator_spec.rb @@ -0,0 +1,154 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + module Providers + module OneDrive + module Validators + RSpec.describe StorageConfigurationValidator, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage, :as_automatically_managed) } + let(:auth_strategy) { Registry["one_drive.authentication.userless"].call } + let(:error) { Results::Error.new(code: error_code, source: self) } + + subject(:validator) { described_class.new(storage) } + + it "returns a GroupValidationResult", vcr: "one_drive/files_query_userless" do + results = validator.call + + expect(results).to be_a(ConnectionValidators::ValidationGroupResult) + expect(results).to be_success + end + + describe "possible error scenarios" do + let(:files_double) { class_double(Queries::FilesQuery) } + let(:input_data) { Input::Files.build(folder: "/").value! } + let(:result) { Success() } + + before do + allow(files_double).to receive(:call).with(storage:, auth_strategy:, input_data:).and_return(result) + end + + context "when the storage isn't configured" do + let(:storage) { create(:one_drive_storage) } + + it "the check fails" do + results = validator.call + expect(results[:storage_configured]).to be_a_failure + expect(results[:storage_configured].code).to eq(:not_configured) + end + end + + context "when diagnostic request fails with an unhandled error" do + let(:error_code) { :error } + let(:result) { Failure(error) } + + before { Registry.stub("one_drive.queries.files", files_double) } + + it "the check fails" do + results = validator.call + + expect(results[:diagnostic_request]).to be_a_failure + expect(results[:diagnostic_request].code).to eq(:unknown_error) + end + + it "logs an error" do + allow(Rails.logger).to receive(:error) + validator.call + + expect(Rails.logger).to have_received(:error).with(/Connection validation failed with unknown/) + end + end + + context "when the tenant id is wrong" do + it "but looks like an actual valid value", vcr: "one_drive/validation_wrong_tenant_id" do + storage.tenant_id = "itdoesnotexists9000.sharepoint.com" + results = described_class.new(storage).call + + expect(results[:tenant_id]).to be_a_failure + expect(results[:tenant_id].code).to eq(:od_tenant_id_invalid) + end + + it "but is blatantly wrong", vcr: "one_drive/validation_absurd_tenant_id" do + storage.tenant_id = "wrong" + results = described_class.new(storage).call + + expect(results[:tenant_id]).to be_a_failure + expect(results[:tenant_id].code).to eq(:od_tenant_id_invalid) + end + end + + context "when the client secret is wrong" do + it "fails the check", vcr: "one_drive/validation_wrong_client_secret" do + storage.oauth_client.client_secret = "wrong" + results = described_class.new(storage).call + + expect(results[:client_secret]).to be_a_failure + expect(results[:client_secret].code).to eq(:client_secret_invalid) + end + end + + context "when the client id is wrong" do + it "fails the check", vcr: "one_drive/validation_wrong_client_id" do + storage.oauth_client.client_id = "wrong" + results = described_class.new(storage).call + + expect(results[:client_id]).to be_a_failure + expect(results[:client_id].code).to eq(:client_id_invalid) + end + end + + context "when the drive id is wrong" do + it "fails when looks malformed", vcr: "one_drive/validation_drive_id_malformed" do + storage.drive_id = "not-a-drive-id" + results = described_class.new(storage).call + + expect(results[:drive_id_format]).to be_a_failure + expect(results[:drive_id_format].code).to eq(:od_drive_id_invalid) + end + + it "fails when is not found", vcr: "one_drive/validation_drive_id_not_found" do + storage.drive_id = "#{storage.drive_id[0..-2]}0" + results = described_class.new(storage).call + + expect(results[:drive_id_exists]).to be_a_failure + expect(results[:drive_id_exists].code).to eq(:od_drive_id_not_found) + end + end + end + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/adapters/registry_spec.rb b/modules/storages/spec/common/storages/adapters/registry_spec.rb new file mode 100644 index 00000000000..a19308bd7a7 --- /dev/null +++ b/modules/storages/spec/common/storages/adapters/registry_spec.rb @@ -0,0 +1,66 @@ +# 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" +require_module_spec_helper + +module Storages + module Adapters + RSpec.describe Registry do + subject(:registry) { described_class } + + describe "error handling" do + context "when a missing key is requested" do + it "raises a MissingContract if it was a contract" do + expect { registry["one_drive.contracts.some_contract"] }.to raise_error Errors::MissingContract + end + + it "raises an OperationNotSupported if a command or query is not registered" do + expect { registry["one_drive.commands.conquer"] }.to raise_error Errors::OperationNotSupported + end + + it "raises an UnknownProvider if the provider is not registered" do + expect { registry["one_google_box.commands.create_folder"] }.to raise_error Errors::UnknownProvider + end + + it "raises a ResolverStandardError if the key cannot be resolved" do + expect { registry["one_drive.graph.rest_api"] }.to raise_error Errors::ResolverStandardError + end + + it "logs the failure" do + allow(Rails.logger).to receive(:error).with("Cannot resolve key one_drive.graph.rest_api.") + + expect { registry["one_drive.graph.rest_api"] }.to raise_error Errors::ResolverStandardError + end + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/ampf_configuration_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/ampf_configuration_validator_spec.rb deleted file mode 100644 index aaba3fc8634..00000000000 --- a/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/ampf_configuration_validator_spec.rb +++ /dev/null @@ -1,172 +0,0 @@ -# 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" -require_module_spec_helper - -module Storages - module Peripherals - module ConnectionValidators - module Nextcloud - RSpec.describe AmpfConfigurationValidator, :webmock do - let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed) } - let(:project_folder_id) { "1337" } - let!(:project_storage) do - create(:project_storage, :as_automatically_managed, project_folder_id:, storage:, project: create(:project)) - end - - let(:files_response) do - ServiceResult.success(result: StorageFiles.new( - [StorageFile.new(id: project_folder_id, name: project_storage.managed_project_folder_name)], - StorageFile.new(id: "root", name: "root"), - [] - )) - end - - let(:required_versions) do - YAML.load_file(Rails.root.join("modules/storages/config/nextcloud_dependencies.yml"))&.dig("dependencies") - end - - let(:capabilities_response) do - NextcloudCapabilities.new( - app_enabled?: true, - app_version: SemanticVersion.parse(required_versions.dig("group_folders_app", "min_version")), - group_folder_enabled?: true, - group_folder_version: SemanticVersion.parse(required_versions.dig("group_folders_app", "min_version")) - ) - end - - subject(:validator) { described_class.new(storage) } - - before do - Registry.stub("nextcloud.queries.files", ->(*) { files_response }) - Registry.stub("nextcloud.queries.capabilities", ->(*) { ServiceResult.success(result: capabilities_response) }) - end - - it "pass all checks" do - expect(validator.call).to be_success - end - - describe "group_folders_app checks" do - before do - Registry.unstub - Registry.stub("nextcloud.queries.files", ->(*) { files_response }) - end - - it "group_folders_app version mismatch", vcr: "nextcloud/capabilities_success" do - absurd_version = { dependencies: { group_folders_app: { min_version: "2099.10.138" } } }.deep_stringify_keys - allow(subject).to receive(:nextcloud_dependencies).and_return(absurd_version) - - results = validator.call - expect(results[:group_folder_app]).to be_a_failure - expect(results[:group_folder_app].code).to eq(:nc_dependency_version_mismatch) - expect(results[:group_folder_app].context[:dependency]).to eq("Group Folders") - end - - it "integration app disabled / missing", vcr: "nextcloud/capabilities_success_group_folder_disabled" do - results = validator.call - - expect(results[:group_folder_app]).to be_a_failure - expect(results[:group_folder_app].code).to eq(:nc_dependency_missing) - expect(results[:group_folder_app].context[:dependency]).to eq("Group Folders") - end - end - - context "if userless authentication fails" do - let(:files_response) { build_failure(code: :unauthorized, payload: nil) } - - it "fails and skips the next checks" do - results = validator.call - - states = results.tally - expect(states).to eq({ success: 2, failure: 1, skipped: 2 }) - expect(results[:userless_access]).to be_failure - expect(results[:userless_access].code).to eq(:nc_userless_access_denied) - end - end - - context "if the files request returns not_found" do - let(:files_response) { build_failure(code: :not_found, payload: nil) } - - it "fails the check" do - results = validator.call - - expect(results[:group_folder_presence]).to be_failure - expect(results[:group_folder_presence].code).to eq(:nc_group_folder_not_found) - end - end - - context "if the files request returns an unknown error" do - let(:files_response) { StorageInteraction::Nextcloud::Util.error(:error) } - - before { allow(Rails.logger).to receive(:error) } - - it "fails the check and logs the error" do - results = validator.call - - expect(results[:files_request]).to be_failure - expect(results[:files_request].code).to eq(:unknown_error) - - expect(Rails.logger).to have_received(:error).with(/Connection validation failed with unknown error/) - end - end - - context "if the files request returns unexpected files" do - let(:files_response) do - ServiceResult.success(result: StorageFiles.new( - [ - StorageFile.new(id: project_folder_id, name: "I am your father"), - StorageFile.new(id: "noooooooooo", name: "testimony_of_luke_skywalker.md") - ], - StorageFile.new(id: "root", name: "root"), - [] - )) - end - - it "warns the user about extraneous folders" do - results = validator.call - - expect(results[:group_folder_contents]).to be_a_warning - expect(results[:group_folder_contents].code).to eq(:nc_unexpected_content) - end - end - - private - - def build_failure(code:, payload:) - data = StorageErrorData.new(source: "query", payload:) - error = StorageError.new(code:, data:) - ServiceResult.failure(result: code, errors: error) - end - end - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/authentication_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/authentication_validator_spec.rb deleted file mode 100644 index 5bde4f6e5c0..00000000000 --- a/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/authentication_validator_spec.rb +++ /dev/null @@ -1,234 +0,0 @@ -# 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" -require_module_spec_helper - -module Storages - module Peripherals - module ConnectionValidators - module Nextcloud - RSpec.describe AuthenticationValidator, :webmock do - subject(:validator) { described_class.new(storage) } - - context "when using OAuth2" do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, - :as_not_automatically_managed, - oauth_client_token_user: user, - origin_user_id: "m.jade@death.star") - end - - before { User.current = user } - - it "passes when the user has a token and the request works", vcr: "nextcloud/user_query_success" do - expect(validator.call).to be_success - end - - it "returns a warning when there's no token for the current user" do - User.current = create(:user) - result = validator.call - - expect(result[:existing_token]).to be_a_warning - expect(result[:existing_token].code).to eq(:nc_oauth_token_missing) - expect(result[:user_bound_request]).to be_skipped - end - - it "returns a failure if the remote call failed" do - Registry.stub("nextcloud.queries.user", ->(_) { ServiceResult.failure(result: :unauthorized) }) - - result = validator.call - expect(result[:user_bound_request]).to be_a_failure - expect(result[:user_bound_request].code).to eq(:nc_oauth_request_unauthorized) - end - end - - context "when using OpenID Connect" do - let(:storage) { create(:nextcloud_storage_configured, :oidc_sso_enabled) } - - let(:user) { create(:user, identity_url: "#{oidc_provider.slug}:123123123123") } - let!(:oidc_provider) { create(:oidc_provider, scope:) } - let!(:saml_provider) { create(:saml_provider) } - let(:scope) { "openid email profile offline_access" } - - before do - User.current = user - - xml_response = Rails.root.join("modules/storages/spec/support/payloads/nextcloud_user_query_success.xml") - stub_request(:get, "#{storage.uri}ocs/v1.php/cloud/user") - .and_return(status: 200, body: File.read(xml_response), headers: { content_type: "text/xml" }) - end - - it "succeeds give the user is provisioned and tokens can be acquired" do - create(:oidc_user_token, user:, extra_audiences: storage.audience) - expect(validator.call).to be_success - end - - describe "error and warning handling" do - it "returns a warning if the current user isn't provisioned" do - user.user_auth_provider_links.destroy_all - result = validator.call - - expect(result[:non_provisioned_user]).to be_warning - expect(result[:non_provisioned_user].code).to eq(:oidc_non_provisioned_user) - - state_count = result.tally - expect(state_count).to eq({ skipped: 4, warning: 1 }) - end - - it "returns a warning if the user is not provisioned by an oidc provider" do - link = user.user_auth_provider_links.first - link.update!(auth_provider_id: saml_provider.id) - - result = validator.call - - expect(result[:provisioned_user_provider]).to be_warning - expect(result[:provisioned_user_provider].code).to eq(:oidc_non_oidc_user) - - state_count = result.tally - expect(state_count).to eq({ success: 1, skipped: 3, warning: 1 }) - end - - context "when the offline_access scope is not configured" do - let(:scope) { "openid email profile" } - - it "returns a warning", :aggregate_failures do - create(:oidc_user_token, user:, extra_audiences: storage.audience) - result = validator.call - - expect(result[:offline_access]).to be_warning - expect(result[:offline_access].code).to eq(:offline_access_scope_missing) - - state_count = result.tally - expect(state_count).to eq({ success: 4, warning: 1 }) - end - end - end - - describe "checks related to the token" do - context "when the token doesn't have the necessary audiences" do - it "returns a validation failure in case the server does not support token exchange" do - create(:oidc_user_token, user:) - result = validator.call - - expect(result[:token_negotiable]).to be_failure - expect(result[:token_negotiable].code).to eq(:oidc_token_acquisition_failed) - end - end - - context "when the existing token requires a refresh" do - let(:expired_storage_token) do - create(:oidc_user_token, user:, extra_audiences: storage.audience, expires_at: 10.hours.ago) - end - - it "tries to refresh the token if it is expired" do - refresh_request = stub_request(:post, oidc_provider.token_endpoint) - .with(body: { grant_type: "refresh_token", - refresh_token: expired_storage_token.refresh_token }) - .and_return_json(status: 200, body: { access_token: "NEW_TOKEN" }) - - expect(validator.call).to be_success - expect(refresh_request).to have_been_requested.once - end - - it "fails when the refresh response is invalid" do - stub_request(:post, oidc_provider.token_endpoint) - .with(body: { grant_type: "refresh_token", refresh_token: expired_storage_token.refresh_token }) - .and_return_json(status: 200, body: { error: "this is a broken endpoint" }) - - result = validator.call - - expect(result[:token_negotiable]).to be_failure - expect(result[:token_negotiable].code).to eq(:oidc_token_refresh_failed) - end - - it "fails when refresh fails" do - stub_request(:post, oidc_provider.token_endpoint) - .with(body: { grant_type: "refresh_token", refresh_token: expired_storage_token.refresh_token }) - .and_return(status: 401) - - result = validator.call - - expect(result[:token_negotiable]).to be_failure - expect(result[:token_negotiable].code).to eq(:oidc_token_refresh_failed) - end - - context "when the server supports token exchange" do - let(:oidc_provider) { create(:oidc_provider, :token_exchange_capable, scope: "offline_access") } - let!(:exchangeable_token) { create(:oidc_user_token, user:, refresh_token: nil) } - - it "favors token exchange when refreshing" do - exchange_request = stub_request(:post, oidc_provider.token_endpoint) - .with(body: hash_including( - grant_type: OpenProject::OpenIDConnect::TOKEN_EXCHANGE_GRANT_TYPE - )) - .and_return_json(status: 200, body: { access_token: "NEW_TOKEN" }) - - expect(validator.call).to be_success - expect(exchange_request).to have_been_requested.once - end - - it "fails if the exchange is met with an unexpected body" do - exchange_request = stub_request(:post, oidc_provider.token_endpoint) - .with(body: hash_including( - grant_type: OpenProject::OpenIDConnect::TOKEN_EXCHANGE_GRANT_TYPE - )) - .and_return_json(status: 200, body: { error: "failed " }) - - result = validator.call - - expect(result[:token_negotiable]).to be_failure - expect(result[:token_negotiable].code).to eq(:oidc_token_exchange_failed) - expect(exchange_request).to have_been_requested.once - end - - it "fails if the exchange fails" do - exchange_request = stub_request(:post, oidc_provider.token_endpoint) - .with(body: hash_including( - grant_type: OpenProject::OpenIDConnect::TOKEN_EXCHANGE_GRANT_TYPE - )) - .and_return(status: 401) - - result = validator.call - - expect(result[:token_negotiable]).to be_failure - expect(result[:token_negotiable].code).to eq(:oidc_token_exchange_failed) - expect(exchange_request).to have_been_requested.once - end - end - end - end - end - end - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/storage_configuration_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/storage_configuration_validator_spec.rb deleted file mode 100644 index 27a4ee0b8fe..00000000000 --- a/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud/storage_configuration_validator_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# 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" -require_module_spec_helper - -module Storages - module Peripherals - module ConnectionValidators - module Nextcloud - RSpec.describe StorageConfigurationValidator, :webmock do - let(:storage) { create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed) } - - subject(:validator) { described_class.new(storage) } - - it "returns a GroupValidationResult", vcr: "nextcloud/capabilities_success" do - results = validator.call - - expect(results).to be_a(ValidationGroupResult) - expect(results).to be_success - end - - describe "possible error scenarios" do - context "when the storage is not configured" do - let(:storage) { create(:nextcloud_storage) } - - it "the check fails" do - results = validator.call - expect(results[:storage_configured]).to be_a_failure - expect(results[:storage_configured].code).to eq(:not_configured) - end - end - - it "base url could not be reached" do - stub_request(:get, UrlBuilder.url(storage.uri, "/ocs/v2.php/cloud/capabilities")) - .to_return(status: 404, body: "Not Found") - - results = validator.call - expect(results[:host_url_accessible]).to be_a_failure - expect(results[:host_url_accessible].code).to eq(:nc_host_not_found) - end - - it "integration app version mismatch", vcr: "nextcloud/capabilities_success" do - absurd_version = { dependencies: { integration_app: { min_version: "2099.10.138" } } }.deep_stringify_keys - allow(subject).to receive(:nextcloud_dependencies).and_return(absurd_version) - - results = validator.call - expect(results[:dependencies_versions]).to be_a_failure - expect(results[:dependencies_versions].code).to eq(:nc_dependency_version_mismatch) - expect(results[:dependencies_versions].context[:dependency]).to eq("Integration OpenProject") - end - - it "integration app disabled / missing", vcr: "nextcloud/capabilities_success_app_disabled" do - results = validator.call - - expect(results[:dependencies_check]).to be_a_failure - expect(results[:dependencies_check].code).to eq(:nc_dependency_missing) - expect(results[:dependencies_check].context[:dependency]).to eq("Integration OpenProject") - end - end - end - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator_spec.rb deleted file mode 100644 index 737e0e2c38d..00000000000 --- a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -# 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" -require_module_spec_helper - -module Storages - module Peripherals - module ConnectionValidators - module OneDrive - RSpec.describe AmpfConfigurationValidator, :webmock do - let(:storage) { create(:sharepoint_dev_drive_storage, :as_automatically_managed) } - let(:auth_strategy) { Registry["one_drive.authentication.userless"].call } - let(:folder_name) { described_class::TEST_FOLDER_NAME } - - subject(:validator) { described_class.new(storage) } - - it "returns a GroupValidationResult", vcr: "one_drive/validator_ampf_clean_run" do - results = validator.call - - expect(results).to be_a(ValidationGroupResult) - expect(results).to be_success - end - - describe "possible error scenarios" do - it "fails when there's unexpected folder and files in the drive", vcr: "one_drive/validator_extraneous_files" do - results = validator.call - - expect(results[:drive_contents]).to be_a_warning - expect(results[:drive_contents].code).to eq(:od_unexpected_content) - end - - it "fails when folders can't be created" do - create_cmd = class_double(StorageInteraction::OneDrive::CreateFolderCommand) - allow(create_cmd).to receive(:call) - .with(storage:, auth_strategy:, folder_name:, parent_location: ParentFolder.root) - .and_return(ServiceResult.failure) - - Registry.stub("one_drive.commands.create_folder", create_cmd) - - results = validator.call - - expect(results[:client_folder_creation]).to be_a_failure - expect(results[:client_folder_creation].code).to eq(:od_client_write_permission_missing) - end - - it "fails when the test folder already exists on the remote", vcr: "one_drive/validator_test_folder_already_exists" do - Registry["one_drive.commands.create_folder"] - .call(storage:, auth_strategy:, folder_name:, parent_location: ParentFolder.root) - - result = validator.call - expect(result[:client_folder_creation]).to be_a_failure - expect(result[:client_folder_creation].code).to eq(:od_test_folder_exists) - expect(result[:client_folder_creation].context[:folder_name]).to eq(folder_name) - ensure - StorageInteraction::OneDrive::DeleteFolderCommand.call(storage:, auth_strategy:, location: created_folder) - end - - it "fails when folders can't be deleted", vcr: "one_drive/validator_create_folder" do - delete_cmd = class_double(StorageInteraction::OneDrive::DeleteFolderCommand) - allow(delete_cmd).to receive(:call).with(storage:, auth_strategy:, location: /.+/) - .and_return(ServiceResult.failure) - - Registry.stub("one_drive.commands.delete_folder", delete_cmd) - - results = validator.call - - expect(results[:client_folder_removal]).to be_a_failure - expect(results[:client_folder_removal].code).to eq(:od_client_cant_delete_folder) - ensure - StorageInteraction::OneDrive::DeleteFolderCommand.call(storage:, auth_strategy:, location: created_folder) - end - end - - private - - def created_folder - Registry["one_drive.queries.files"].call(storage:, auth_strategy:, folder: ParentFolder.root).on_success do - folder = it.result.files.detect { |file| file.name.include?(folder_name) } - - return folder.id - end - end - end - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator_spec.rb deleted file mode 100644 index 88eaf1ce6d1..00000000000 --- a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -# 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" -require_module_spec_helper - -module Storages - module Peripherals - module ConnectionValidators - module OneDrive - RSpec.describe StorageConfigurationValidator, :webmock do - let(:storage) { create(:sharepoint_dev_drive_storage, :as_automatically_managed) } - let(:auth_strategy) { Registry["one_drive.authentication.userless"].call } - - subject(:validator) { described_class.new(storage) } - - it "returns a GroupValidationResult", vcr: "one_drive/files_query_userless" do - results = validator.call - - expect(results).to be_a(ValidationGroupResult) - expect(results).to be_success - end - - describe "possible error scenarios" do - let(:files_double) { class_double(StorageInteraction::OneDrive::FilesQuery) } - let(:result) { ServiceResult.success } - - before do - allow(files_double).to receive(:call).with(storage:, auth_strategy:, folder: ParentFolder.root).and_return(result) - end - - context "when the storage isn't configured" do - let(:storage) { create(:one_drive_storage) } - - it "the check fails" do - results = validator.call - expect(results[:storage_configured]).to be_a_failure - expect(results[:storage_configured].code).to eq(:not_configured) - end - end - - context "when diagnostic request fails with an unhandled error" do - let(:result) { ServiceResult.failure(result: :error, errors: StorageError.new(code: :error)) } - - before { Registry.stub("one_drive.queries.files", files_double) } - - it "the check fails" do - results = validator.call - - expect(results[:diagnostic_request]).to be_a_failure - expect(results[:diagnostic_request].code).to eq(:unknown_error) - end - - it "logs an error" do - allow(Rails.logger).to receive(:error) - validator.call - - expect(Rails.logger).to have_received(:error).with(/Connection validation failed with unknown/) - end - end - - context "when the tenant id is wrong" do - it "but looks like an actual valid value", vcr: "one_drive/validation_wrong_tenant_id" do - storage.tenant_id = "itdoesnotexists9000.sharepoint.com" - results = described_class.new(storage).call - - expect(results[:tenant_id]).to be_a_failure - expect(results[:tenant_id].code).to eq(:od_tenant_id_invalid) - end - - it "but is blatantly wrong", vcr: "one_drive/validation_absurd_tenant_id" do - storage.tenant_id = "wrong" - results = described_class.new(storage).call - - expect(results[:tenant_id]).to be_a_failure - expect(results[:tenant_id].code).to eq(:od_tenant_id_invalid) - end - end - - context "when the client secret is wrong" do - it "fails the check", vcr: "one_drive/validation_wrong_client_secret" do - storage.oauth_client.client_secret = "wrong" - results = described_class.new(storage).call - - expect(results[:client_secret]).to be_a_failure - expect(results[:client_secret].code).to eq(:client_secret_invalid) - end - end - - context "when the client id is wrong" do - it "fails the check", vcr: "one_drive/validation_wrong_client_id" do - storage.oauth_client.client_id = "wrong" - results = described_class.new(storage).call - - expect(results[:client_id]).to be_a_failure - expect(results[:client_id].code).to eq(:client_id_invalid) - end - end - - context "when the drive id is wrong" do - it "fails when looks malformed", vcr: "one_drive/validation_drive_id_malformed" do - storage.drive_id = "not-a-drive-id" - results = described_class.new(storage).call - - expect(results[:drive_id_format]).to be_a_failure - expect(results[:drive_id_format].code).to eq(:od_drive_id_invalid) - end - - it "fails when is not found", vcr: "one_drive/validation_drive_id_not_found" do - storage.drive_id = "#{storage.drive_id[0..-2]}0" - results = described_class.new(storage).call - - expect(results[:drive_id_exists]).to be_a_failure - expect(results[:drive_id_exists].code).to eq(:od_drive_id_not_found) - end - end - end - end - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/registry_spec.rb b/modules/storages/spec/common/storages/peripherals/registry_spec.rb deleted file mode 100644 index 3e2e5c6e24f..00000000000 --- a/modules/storages/spec/common/storages/peripherals/registry_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::Registry, :webmock do - using Storages::Peripherals::ServiceResultRefinements - - let(:user) { create(:user) } - let(:url) { "https://example.com" } - let(:origin_user_id) { "admin" } - let(:storage) { build(:nextcloud_storage, :as_automatically_managed, host: url, password: "OpenProjectSecurePassword") } - - subject(:registry) { described_class } - - context "when a key is not registered" do - it "raises a OperationNotSupported for a non-existent command/query" do - expect { registry.resolve("nextcloud.commands.destroy_alderaan") }.to raise_error Storages::Errors::OperationNotSupported - expect { registry.resolve("nextcloud.queries.alderaan") }.to raise_error Storages::Errors::OperationNotSupported - end - - it "raises a MissingContract for a non-existent contract" do - expect { registry["warehouse.contracts.storage"] }.to raise_error Storages::Errors::MissingContract - end - - it "raises a ResolverStandardError in all other cases" do - expect { registry.resolve("it.is.a.trap") }.to raise_error Storages::Errors::ResolverStandardError - end - end - - describe "#delete_folder_command" do - let(:auth_strategy) { Storages::Peripherals::StorageInteraction::AuthenticationStrategies::BasicAuth.strategy } - - before do - stub_request(:delete, "https://example.com/remote.php/dav/files/OpenProject/OpenProject/Folder%201") - .with(headers: { "Authorization" => "Basic T3BlblByb2plY3Q6T3BlblByb2plY3RTZWN1cmVQYXNzd29yZA==" }) - .to_return(status: 204, body: "", headers: {}) - end - - describe "with Nextcloud storage type selected" do - it "deletes the folder" do - result = registry.resolve("nextcloud.commands.delete_folder") - .call(storage:, auth_strategy:, location: "OpenProject/Folder 1") - expect(result).to be_success - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_method_selector_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_method_selector_spec.rb deleted file mode 100644 index 92ffe7a37b7..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_method_selector_spec.rb +++ /dev/null @@ -1,122 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector do - subject { described_class.new(storage:, user:) } - - context "if user is provisioned by an IDP" do - let(:provider) { create(:oidc_provider) } - let(:user) { create(:user, authentication_provider: provider) } - - context "if file storage is configured for sso only" do - let(:storage) { create(:nextcloud_storage, :oidc_sso_enabled) } - - it { is_expected.to be_sso } - it { is_expected.not_to be_storage_oauth } - - it "indicates an authentication method of :sso" do - expect(subject.authentication_method).to eq(:sso) - end - end - - context "if file storage is configured for sso and oauth" do - let(:storage) { create(:nextcloud_storage_configured, :oidc_sso_with_fallback) } - - it { is_expected.to be_sso } - it { is_expected.not_to be_storage_oauth } - - it "indicates an authentication method of :sso" do - expect(subject.authentication_method).to eq(:sso) - end - end - - context "if file storage is configured for oauth only" do - let(:storage) { create(:nextcloud_storage_configured) } - - it { is_expected.not_to be_sso } - it { is_expected.to be_storage_oauth } - - it "indicates an authentication method of :storage_oauth" do - expect(subject.authentication_method).to eq(:storage_oauth) - end - end - end - - context "if user is local" do - let(:user) { create(:user) } - - context "if file storage is configured for sso only" do - let(:storage) { create(:nextcloud_storage, :oidc_sso_enabled) } - - it { is_expected.not_to be_sso } - it { is_expected.not_to be_storage_oauth } - - it "indicates an authentication method of :sso" do - expect(subject.authentication_method).to be_nil - end - end - - context "if file storage is configured for sso and oauth" do - let(:storage) { create(:nextcloud_storage_configured, :oidc_sso_with_fallback) } - - it { is_expected.not_to be_sso } - it { is_expected.to be_storage_oauth } - - it "indicates an authentication method of :storage_oauth" do - expect(subject.authentication_method).to eq(:storage_oauth) - end - end - - context "if file storage is configured for oauth only" do - let(:storage) { create(:nextcloud_storage_configured) } - - it { is_expected.not_to be_sso } - it { is_expected.to be_storage_oauth } - - it "indicates an authentication method of :storage_oauth" do - expect(subject.authentication_method).to eq(:storage_oauth) - end - end - - context "if file storage is configured for oauth only, but client and app not fully configured" do - let(:storage) { create(:nextcloud_storage) } - - it { is_expected.not_to be_sso } - it { is_expected.to be_storage_oauth } - - it "indicates an authentication method of :storage_oauth" do - expect(subject.authentication_method).to eq(:storage_oauth) - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_spec.rb deleted file mode 100644 index 6aa0dd5e471..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_spec.rb +++ /dev/null @@ -1,325 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Authentication, :webmock do - let(:user) { create(:user) } - - shared_examples_for "successful response" do |refreshed: false| - it "must #{refreshed ? 'refresh token and ' : ''}return success" do - result = described_class[auth_strategy].call(storage:, http_options:) { |http| make_request(http) } - expect(result).to be_success - expect(result.result).to eq("EXPECTED_RESULT") - end - end - - context "with a Nextcloud storage" do - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:request_url) { "#{storage.uri}ocs/v1.php/cloud/user" } - let(:http_options) { { headers: { "OCS-APIRequest" => "true", "Accept" => "application/json" } } } - - context "with basic auth strategy" do - let(:auth_strategy) { Storages::Peripherals::StorageInteraction::AuthenticationStrategies::BasicAuth.strategy } - - context "with valid credentials", vcr: "auth/nextcloud/basic_auth" do - before do - # Those values are only used to record the vcr cassette - storage.username = "admin" - storage.password = "admin" - end - - it_behaves_like "successful response" - end - - context "with empty username and password" do - it "must return error" do - result = described_class[auth_strategy].call(storage:, http_options:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:error) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::BasicAuth) - end - end - - context "with invalid username and/or password", vcr: "auth/nextcloud/basic_auth_password_invalid" do - before do - # Those values are only used to record the vcr cassette - storage.username = "admin" - storage.password = "YouShallNot(Multi)Pass" - end - - it "must return unauthorized" do - result = described_class[auth_strategy].call(storage:, http_options:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:unauthorized) - expect(error.data.source).to be("EXECUTING_QUERY") - end - end - end - - context "with user token strategy" do - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - context "with incomplete storage configuration (missing oauth client)" do - let(:storage) { create(:nextcloud_storage) } - - it "must return error" do - result = described_class[auth_strategy].call(storage:, http_options:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:error) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken) - end - end - - context "with not existent oauth token" do - let(:user_without_token) { create(:user) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken - .strategy - .with_user(user_without_token) - end - - it "must return unauthorized" do - result = described_class[auth_strategy].call(storage:, http_options:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:unauthorized) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken) - end - end - - context "with invalid oauth refresh token", vcr: "auth/nextcloud/user_token_refresh_token_invalid" do - before { storage } - - it "must return unauthorized" do - result = described_class[auth_strategy].call(storage:, http_options:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:unauthorized) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken) - end - - it "responds with error when lock could not be obtained timely" do - strategy = described_class[auth_strategy] - - allow(OpenProject::Mutex).to receive(:with_advisory_lock).and_return(false) - - result = strategy.call(storage:, http_options:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:error) - expect(error.data.source).to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken) - expect(error.log_message).to eq("Lock has not been acquired in 4 seconds. Refresh token is being updated at the moment by another thread.") - end - - it "logs, retries once, raises exception if race condition happens" do - token = user.oauth_client_tokens.first - strategy = described_class[auth_strategy] - - allow(Rails.logger).to receive(:error) - allow(strategy).to receive(:current_token).and_return(ServiceResult.success(result: token)) - allow(token).to receive(:destroy).and_raise(ActiveRecord::StaleObjectError).twice - - expect do - strategy.call(storage:, http_options:) { |http| make_request(http) } - end.to raise_error(ActiveRecord::StaleObjectError) - - expect(Rails.logger) - .to have_received(:error) - .with("# happend for User ##{user.id} #{user.name}").once - end - end - - context "with invalid oauth access token", vcr: "auth/nextcloud/user_token_access_token_invalid" do - it_behaves_like "successful response", refreshed: true - - context "when updating token in openproject database fails" do - it "responds with error" do - storage - token = user.oauth_client_tokens.first - strategy = described_class[auth_strategy] - allow(strategy).to receive(:current_token).and_return(ServiceResult.success(result: token)) - allow(token).to receive(:update).and_return(false) - - result = strategy.call(storage:, http_options:) { |http| make_request(http) } - expect(result).to be_failure - expect(result.result).to eq(:error) - expect(result.errors.code).to eq(:error) - expect(result.errors.log_message).to eq("Error while persisting updated access token.") - end - end - end - end - end - - context "with a OneDrive/SharePoint storage" do - let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - let(:http_options) { {} } - - context "with client credentials strategy" do - let(:request_url) { "#{storage.uri}v1.0/drives" } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy - end - - context "with valid oauth credentials", vcr: "auth/one_drive/client_credentials" do - it_behaves_like "successful response" - end - - context "with invalid client secret", vcr: "auth/one_drive/client_credentials_invalid_client_secret" do - it "must return unauthorized" do - result = described_class[auth_strategy].call(storage:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:unauthorized) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials) - end - end - - context "with invalid client id", vcr: "auth/one_drive/client_credentials_invalid_client_id" do - it "must return unauthorized" do - result = described_class[auth_strategy].call(storage:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:unauthorized) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials) - end - end - end - - context "with user token strategy" do - let(:request_url) { "#{storage.uri}v1.0/me" } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - context "with valid access token", vcr: "auth/one_drive/user_token" do - it_behaves_like "successful response" - end - - context "with incomplete storage configuration (missing oauth client)" do - let(:storage) { create(:one_drive_storage) } - - it "must return error" do - result = described_class[auth_strategy].call(storage:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:error) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken) - end - end - - context "with not existent oauth token" do - let(:user_without_token) { create(:user) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken - .strategy - .with_user(user_without_token) - end - - it "must return unauthorized" do - result = described_class[auth_strategy].call(storage:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:unauthorized) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken) - end - end - - context "with invalid oauth refresh token", vcr: "auth/one_drive/user_token_refresh_token_invalid" do - it "must return unauthorized" do - result = described_class[auth_strategy].call(storage:) { |http| make_request(http) } - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:unauthorized) - expect(error.data.source) - .to be(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken) - end - end - - context "with invalid oauth access token", vcr: "auth/one_drive/user_token_access_token_invalid" do - it_behaves_like "successful response", refreshed: true - end - end - end - - private - - def make_request(http) - handle_response http.get(request_url) - end - - def handle_response(response) - case response - in { status: 200..299 } - ServiceResult.success(result: "EXPECTED_RESULT") - in { status: 401 } - error(:unauthorized) - in { status: 403 } - error(:forbidden) - in { status: 404 } - error(:not_found) - else - error(:error) - end - end - - def error(code) - data = Storages::StorageErrorData.new(source: "EXECUTING_QUERY") - ServiceResult.failure(result: code, errors: Storages::StorageError.new(code:, data:)) - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/nextcloud_strategies/user_bound_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/nextcloud_strategies/user_bound_spec.rb deleted file mode 100644 index 7078a2774a5..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/nextcloud_strategies/user_bound_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::AuthenticationStrategies::NextcloudStrategies::UserBound do - context "if user is provisioned by an IDP" do - let(:provider) { create(:oidc_provider) } - let(:user) { create(:user, authentication_provider: provider) } - - context "if file storage is configured for sso only" do - let(:storage) { create(:nextcloud_storage, :oidc_sso_enabled) } - - it "must use an SsoUserToken strategy" do - strategy = described_class.call(user:, storage:) - expect(strategy.key).to eq(:sso_user_token) - end - end - - context "if file storage is configured for sso and oauth" do - let(:storage) { create(:nextcloud_storage_configured, :oidc_sso_with_fallback) } - - it "must use an SsoUserToken strategy" do - strategy = described_class.call(user:, storage:) - expect(strategy.key).to eq(:sso_user_token) - end - end - - context "if file storage is configured for oauth only" do - let(:storage) { create(:nextcloud_storage_configured) } - - it "must use an OAuthUserToken strategy" do - strategy = described_class.call(user:, storage:) - expect(strategy.key).to eq(:oauth_user_token) - end - end - end - - context "if user is local" do - let(:user) { create(:user) } - - context "if file storage is configured for sso only" do - let(:storage) { create(:nextcloud_storage, :oidc_sso_enabled) } - - it "must return the failure strategy" do - strategy = described_class.call(user:, storage:) - expect(strategy.key).to eq(:failure) - end - end - - context "if file storage is configured for sso and oauth" do - let(:storage) { create(:nextcloud_storage_configured, :oidc_sso_with_fallback) } - - it "must use an OAuthUserToken strategy" do - strategy = described_class.call(user:, storage:) - expect(strategy.key).to eq(:oauth_user_token) - end - end - - context "if file storage is configured for oauth only" do - let(:storage) { create(:nextcloud_storage_configured) } - - it "must use an OAuthUserToken strategy" do - strategy = described_class.call(user:, storage:) - expect(strategy.key).to eq(:oauth_user_token) - end - end - - context "if file storage is configured for oauth only, but client and app not fully configured" do - let(:storage) { create(:nextcloud_storage) } - - it "returns the configured strategy" do - strategy = described_class.call(user:, storage:) - expect(strategy.key).to eq(:oauth_user_token) - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_client_credentials_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_client_credentials_spec.rb deleted file mode 100644 index 07f127a080f..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/oauth_client_credentials_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials, :webmock do - let(:storage) { create(:sharepoint_dev_drive_storage) } - let(:cache_key) { "storage.#{storage.id}.httpx_access_token" } - - subject(:strategy) { described_class.new(true) } - - context "when the attempted request fails with a 403" do - before do - stub_request(:post, "https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token") - .and_return(status: 200, body: token_json, headers: { content_type: "application/json" }) - - stub_request(:get, "https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root") - .and_return(status: 403) - end - - it "does not cache the result" do - expect(Rails.cache.fetch(cache_key)).to be_nil - - strategy.call(storage:) do |http| - http.get("https://graph.microsoft.com/v1.0/drives/#{storage.drive_id}/root").raise_for_status - rescue HTTPX::Error - ServiceResult.failure - end - - expect(Rails.cache.fetch(cache_key)).to be_nil - end - - it "returns the result of the operation" do - result = strategy.call(storage:) do |http| - http.get("https://graph.microsoft.com/v1.0/drives/#{storage.drive_id}/root").raise_for_status - rescue HTTPX::Error - ServiceResult.failure(result: "It failed") - end - - expect(result).to be_failure - expect(result.result).to eq("It failed") - end - - it "clears an already existing token" do - Rails.cache.write(cache_key, "BORKED_TOKEN") - - strategy.call(storage:) do |http| - http.get("https://graph.microsoft.com/v1.0/drives/#{storage.drive_id}/root").raise_for_status - rescue HTTPX::Error - ServiceResult.failure(result: :forbidden) - end - - expect(Rails.cache.fetch(cache_key)).to be_nil - end - end - - context "when the attempted request works" do - before do - Rails.cache.clear - - stub_request(:post, "https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token") - .and_return(status: 200, body: token_json, headers: { content_type: "application/json" }) - - stub_request(:get, "https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root") - .and_return(status: 200, body: { data: "bunch of data" }.to_json, headers: { content_type: "application/json" }) - end - - it "caches the generated token" do - expect(Rails.cache.fetch(cache_key)).to be_nil - - strategy.call(storage:) do |http| - http.get("https://graph.microsoft.com/v1.0/drives/#{storage.drive_id}/root").raise_for_status - ServiceResult.success - end - - expect(Rails.cache.fetch(cache_key)).to eq("TOTALLY_VALID_TOKEN") - end - - it "returns the result of the operation" do - result = strategy.call(storage:) do |http| - http.get("https://graph.microsoft.com/v1.0/drives/#{storage.drive_id}/root").raise_for_status - ServiceResult.success(result: "It works") - end - - expect(result).to be_success - expect(result.result).to eq("It works") - end - end - - private - - def token_json - '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"TOTALLY_VALID_TOKEN"}' - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/sso_user_token_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/sso_user_token_spec.rb deleted file mode 100644 index a2e816787a5..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/authentication_strategies/sso_user_token_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::AuthenticationStrategies::SsoUserToken do - include Dry::Monads[:result] - - let(:storage) { create(:nextcloud_storage) } - - subject(:strategy) { described_class.new(create(:user)) } - - before do - service = instance_double(OpenIDConnect::UserTokens::FetchService) - allow(OpenIDConnect::UserTokens::FetchService).to receive(:new).and_return(service) - allow(service).to receive(:access_token_for).with(audience: storage.audience).and_return(access_token_result) - end - - context "if access token can be fetched successfully" do - let(:token) { "my_access_token" } - let(:access_token_result) { Success(token) } - - it "must yield with access token" do - was_yielded = false - - strategy.call(storage:) do |http| - was_yielded = true - expect(http.instance_variable_get(:@options).headers["authorization"]).to eq("Bearer #{token}") - end - - expect(was_yielded).to be_truthy - end - end - - context "if fetching access token fails" do - let(:error) { "U shall not pass!" } - let(:access_token_result) { Failure(error) } - - it "must not yield and return failure" do - was_yielded = false - result = strategy.call(storage:) { was_yielded = true } - - expect(was_yielded).to be_falsy - expect(result).to be_failure - expect(result.result).to eq(:unauthorized) - expect(result.errors).to be_a(Storages::StorageError) - expect(result.errors.data).to eq(error) - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/inputs/set_permissions_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/inputs/set_permissions_spec.rb deleted file mode 100644 index e2fd2f518ce..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/inputs/set_permissions_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# 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 Storages::Peripherals::StorageInteraction::Inputs::SetPermissions do - describe ".new" do - it "discourages direct instantiation" do - expect { described_class.new(file_id: "file_id", user_permissions: []) } - .to raise_error(NoMethodError, /private method 'new'/) - end - end - - describe ".build" do - it "creates a success result for valid input data" do - expect(described_class.build(file_id: "1337", user_permissions: [])).to be_success - expect(described_class.build(file_id: "1337", - user_permissions: [{ user_id: "dart_vader", permissions: [] }])).to be_success - expect(described_class - .build( - file_id: "1337", - user_permissions: [ - { user_id: "dart_vader", permissions: %i[read_files write_files delete_files] }, - { group_id: "stormtroopers", permissions: [:read_files] } - ] - )).to be_success - end - - it "creates a failure result for invalid input data" do - expect(described_class.build(file_id: nil, user_permissions: [])).to be_failure - expect(described_class.build(file_id: "", user_permissions: [])).to be_failure - expect(described_class.build(file_id: "1337", user_permissions: {})).to be_failure - - expect(described_class.build(file_id: "1337", user_permissions: [:read_files])).to be_failure - expect(described_class.build(file_id: "1337", user_permissions: [{ user: "rey", permissions: [] }])).to be_failure - expect(described_class.build(file_id: "1337", user_permissions: [{ user_id: "rey" }])).to be_failure - expect(described_class.build(file_id: "1337", - user_permissions: [{ user_id: "rey", permissions: [:read] }])).to be_failure - expect(described_class.build(file_id: "1337", - user_permissions: [{ user_id: "rey", permissions: {} }])).to be_failure - - expect(described_class.build(file_id: "1337", user_permissions: [{ permissions: [:read_files] }])).to be_failure - expect(described_class - .build( - file_id: "1337", - user_permissions: [{ user_id: "rey", group_id: "jedi", permissions: [:read_files] }] - )).to be_failure - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/inputs/upload_data_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/inputs/upload_data_spec.rb deleted file mode 100644 index 4a44fd9d64a..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/inputs/upload_data_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Inputs::UploadData do - subject(:input) { described_class } - - it "has .new as a private method" do - expect { input.new(nil, nil) }.to raise_error NoMethodError - end - - it "returns a Success(UploadData)" do - result = input.build(folder_id: "1337", file_name: "i_am_file_with_a_name.txt") - - expect(result).to be_success - upload_data = result.value! - expect(upload_data.folder_id).to eq("1337") - expect(upload_data.file_name).to eq("i_am_file_with_a_name.txt") - end - - context "when invalid" do - context "with a nil file name" do - let(:kwargs) { { folder_id: "42", file_name: nil } } - - it "returns a failure" do - result = input.build(**kwargs) - expect(result).to be_failure - end - - it "contains the specific error" do - validation_result = input.build(**kwargs).failure - expect(validation_result.errors[:file_name]).to eq(["must be filled"]) - end - end - - context "with a empty file name" do - let(:kwargs) { { folder_id: "42", file_name: "" } } - - it "returns a failure" do - result = input.build(**kwargs) - expect(result).to be_failure - end - - it "contains the specific error" do - validation_result = input.build(**kwargs).failure - expect(validation_result.errors[:file_name]).to eq(["must be filled"]) - end - end - - context "with an empty folder_id" do - let(:kwargs) { { folder_id: "", file_name: "file_name.txt" } } - - it "returns a failure" do - result = input.build(**kwargs) - expect(result).to be_failure - end - - it "contains the specific error" do - validation_result = input.build(**kwargs).failure - expect(validation_result.errors[:folder_id]).to eq(["must be filled"]) - end - end - - context "with a nil folder id" do - let(:kwargs) { { folder_id: nil, file_name: "file_name.txt" } } - - it "returns a failure" do - result = input.build(**kwargs) - expect(result).to be_failure - expect(result.failure.errors[:folder_id]).to eq(["must be filled"]) - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/add_user_to_group_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/add_user_to_group_command_spec.rb deleted file mode 100644 index 00cd9d46c1b..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/add_user_to_group_command_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::AddUserToGroupCommand, :webmock do - include NextcloudGroupUserHelper - - let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed, username: "vcr") } - let(:auth_strategy) { Storages::Peripherals::Registry.resolve("nextcloud.authentication.userless").call } - - describe "basic command setup" do - it "is registered as commands.add_user_to_group" do - expect(Storages::Peripherals::Registry - .resolve("#{storage}.commands.add_user_to_group")).to eq(described_class) - end - - it "responds to #call with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq user], - %i[keyreq group]) - end - end - - shared_examples_for "failing request" do |error_code:| - it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, user:, group:) - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(error_code) - expect(error.data.source).to eq(described_class) - end - end - - context "if group exists", vcr: "nextcloud/add_user_to_group_success" do - let(:user) { "m.jade@death.star" } - let(:group) { "Sith Assassins" } - - before do - create_group(auth, storage, group) - end - - after do - remove_group(auth, storage, group) - end - - it "returns a success" do - members = group_members(group) - expect(members).not_to include(user) - - result = described_class.call(storage:, auth_strategy:, user:, group:) - expect(result).to be_success - - members = group_members(group) - expect(members).to include(user) - end - end - - context "if target group does not exist", vcr: "nextcloud/add_user_to_group_not_existing_group" do - let(:user) { "m.jade@death.star" } - let(:group) { "Sith Assassins" } - - it_behaves_like "failing request", error_code: :group_does_not_exist - end - - context "if user does not exist", vcr: "nextcloud/add_user_to_group_not_existing_user" do - let(:user) { "this is not the user you are looking for" } - let(:group) { "Sith Assassins" } - - before do - create_group(auth, storage, group) - end - - after do - remove_group(auth, storage, group) - end - - it_behaves_like "failing request", error_code: :user_does_not_exist - end - - private - - def auth = Storages::Peripherals::StorageInteraction::Authentication[auth_strategy] - - def group_members(group) - Storages::Peripherals::StorageInteraction::Nextcloud::GroupUsersQuery - .call(storage:, auth_strategy:, group:) - .result - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/capabilities_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/capabilities_query_spec.rb deleted file mode 100644 index be54a814c0b..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/capabilities_query_spec.rb +++ /dev/null @@ -1,117 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CapabilitiesQuery, :webmock do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Noop.strategy - end - - it "is registered as queries.capabilities" do - expect(Storages::Peripherals::Registry.resolve("nextcloud.queries.capabilities")).to eq(described_class) - end - - it "responds to #call with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy]) - end - - shared_examples_for "a successful Nextcloud capabilities response" do - it "returns a capabilities object" do - result = described_class.call(storage:, auth_strategy:) - - expect(result).to be_success - - response = result.result - expect(response).to be_a(Storages::NextcloudCapabilities) - expect(response.app_enabled?).to eq(app_enabled?) - expect(response.app_version).to eq(app_version) - expect(response.group_folder_enabled?).to eq(group_folder_enabled?) - expect(response.group_folder_version).to eq(group_folder_version) - end - end - - context "if both apps are installed", vcr: "nextcloud/capabilities_success" do - let(:app_enabled?) { true } - let(:app_version) { Storages::SemanticVersion.parse("2.6.3") } - let(:group_folder_enabled?) { true } - let(:group_folder_version) { Storages::SemanticVersion.parse("16.0.7") } - - it_behaves_like "a successful Nextcloud capabilities response" - end - - context "if group folder app is installed but disabled", vcr: "nextcloud/capabilities_success_group_folder_disabled" do - let(:app_enabled?) { true } - let(:app_version) { Storages::SemanticVersion.parse("2.6.3") } - let(:group_folder_enabled?) { false } - let(:group_folder_version) { nil } - - it_behaves_like "a successful Nextcloud capabilities response" - end - - context "if group folder app is not installed", vcr: "nextcloud/capabilities_success_group_folder_not_installed" do - let(:app_enabled?) { true } - let(:app_version) { Storages::SemanticVersion.parse("2.6.3") } - let(:group_folder_enabled?) { false } - let(:group_folder_version) { nil } - - it_behaves_like "a successful Nextcloud capabilities response" - end - - context "if integration app is not installed", vcr: "nextcloud/capabilities_success_app_disabled" do - let(:app_enabled?) { false } - let(:app_version) { nil } - let(:group_folder_enabled?) { false } - let(:group_folder_version) { nil } - - it_behaves_like "a successful Nextcloud capabilities response" - end - - context "if response contains invalid version data", vcr: "nextcloud/capabilities_invalid_data" do - it "returns a failure" do - result = described_class.call(storage:, auth_strategy:) - - expect(result).to be_failure - - error = result.errors - expect(error).to be_a(Storages::StorageError) - expect(error.code).to eq(:invalid_version_number) - expect(error.log_message).to include("not a valid version string") - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command_spec.rb deleted file mode 100644 index ade8325b5eb..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/copy_template_folder_command_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CopyTemplateFolderCommand, :webmock do - using Storages::Peripherals::ServiceResultRefinements - - let(:user) { create(:user) } - let(:url) { "https://example.com" } - let(:origin_user_id) { "OpenProject" } - let(:storage) { build(:nextcloud_storage, :as_automatically_managed, host: url, password: "OpenProjectSecurePassword") } - - let(:source_path) { "/source-of-fun" } - let(:destination_path) { "/boring-destination" } - let(:source_url) { "#{url}/remote.php/dav/files/#{CGI.escape(origin_user_id)}#{source_path}" } - let(:destination_url) { "#{url}/remote.php/dav/files/#{CGI.escape(origin_user_id)}#{destination_path}" } - let(:auth_strategy) { Storages::Peripherals::StorageInteraction::AuthenticationStrategies::NextcloudStrategies::UserLess.call } - - subject { described_class.new(storage) } - - describe "#call" do - before { stub_request(:head, destination_url).to_return(status: 404) } - - describe "parameter validation" do - it "source_path cannot be blank" do - result = subject.call(auth_strategy:, source_path: "", destination_path: "/destination") - - expect(result).to be_failure - expect(result.errors.log_message).to eq("Source and destination paths must be present.") - end - - it "destination_path cannot blank" do - result = subject.call(auth_strategy:, source_path: "/source", destination_path: "") - - expect(result).to be_failure - expect(result.errors.log_message).to eq("Source and destination paths must be present.") - end - end - - describe "remote server overwrite protection" do - it "destination_path must not exist on the remote server" do - stub_request(:head, destination_url).to_return(status: 200) - result = subject.call(auth_strategy:, source_path:, destination_path:) - - expect(result).to be_failure - expect(result.errors.log_message).to eq("The copy would overwrite an already existing folder") - end - end - - context "when the folder is copied successfully" do - let(:successful_propfind) do - <<~XML - - - - /remote.php/dav/files/#{CGI.escape(origin_user_id)}#{destination_path} - - - 349 - - HTTP/1.1 200 OK - - - - /remote.php/dav/files/#{CGI.escape(origin_user_id)}#{destination_path}/Dinge/ - - - 783 - - HTTP/1.1 200 OK - - - - XML - end - - before do - stub_request(:copy, source_url).to_return(status: 201) - stub_request(:propfind, destination_url).to_return(status: 200, body: successful_propfind) - end - - it "must be successful" do - result = subject.call(auth_strategy:, source_path:, destination_path:) - - expect(result).to be_success - expect(result.result.id).to eq("349") - end - end - - describe "error handling" do - before do - body = <<~XML - - - Sabre\\DAV\\Exception\\Conflict - The destination node is not found - - XML - stub_request(:copy, source_url).to_return(status: 409, body:, headers: { "Content-Type" => "application/xml" }) - end - - it "returns a :conflict failure if the copy fails" do - result = subject.call(auth_strategy:, source_path:, destination_path:) - - expect(result).to be_failure - expect(result.errors.code).to eq(:conflict) - expect(result.errors.log_message).to eq("The destination node is not found") - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command_spec.rb deleted file mode 100644 index 85d6d73b2e7..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/create_folder_command_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand, :webmock do - describe "admin user" do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - it_behaves_like "create_folder_command: basic command setup" - - context "when creating a folder in the root", vcr: "nextcloud/create_folder_root" do - let(:folder_name) { "Földer CreatedBy Çommand" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } - let(:path) { "/#{folder_name}" } - - it_behaves_like "create_folder_command: successful folder creation" - end - - context "when creating a folder in a parent folder", vcr: "nextcloud/create_folder_parent" do - let(:folder_name) { "Földer CreatedBy Çommand" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("/Folder") } - let(:path) { "/Folder/#{folder_name}" } - - it_behaves_like "create_folder_command: successful folder creation" - end - - context "when creating a folder in a non-existing parent folder", vcr: "nextcloud/create_folder_parent_not_found" do - let(:folder_name) { "New Folder" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("/DeathStar3") } - let(:error_source) { described_class } - - it_behaves_like "create_folder_command: parent not found" - end - - context "when folder already exists", vcr: "nextcloud/create_folder_already_exists" do - let(:folder_name) { "Folder" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } - let(:error_source) { described_class } - - it_behaves_like "create_folder_command: folder already exists" - end - end - - # For the VCR tests in this block it's necessary to - # create a user in Nextcloud with the account name `member@example1` - # and use its oauth access & refresh tokens on .env.test.local - describe "user with custom origin name" do - let(:user) { create(:user) } - let(:storage) do - create( - :nextcloud_storage_with_local_connection, - :as_not_automatically_managed, - origin_user_id: "member@example1", - oauth_client_token_user: user - ) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - context "when creating a folder as a non admin user", vcr: "nextcloud/create_folder_member" do - let(:folder_name) { "Földer CreatedBy Çommand" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } - let(:path) { "/#{folder_name}" } - - it_behaves_like "create_folder_command: successful folder creation" - end - end - - private - - def delete_created_folder(folder) - Storages::Peripherals::Registry - .resolve("nextcloud.commands.delete_folder") - .call(storage:, auth_strategy:, location: folder.location) - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/delete_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/delete_folder_command_spec.rb deleted file mode 100644 index e292e5427ca..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/delete_folder_command_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::DeleteFolderCommand, :vcr, :webmock do - using Storages::Peripherals::ServiceResultRefinements - - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - it "is registered as commands.nextcloud.delete_folder" do - expect(Storages::Peripherals::Registry.resolve("nextcloud.commands.delete_folder")).to eq(described_class) - end - - describe "#call" do - it "responds with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq location]) - end - - it "deletes a folder", vcr: "nextcloud/delete_folder" do - parent_location = Storages::Peripherals::ParentFolder.new("/") - - Storages::Peripherals::Registry - .resolve("nextcloud.commands.create_folder") - .call(storage:, auth_strategy:, folder_name: "To Be Deleted Soon", parent_location:) - - result = described_class.call(storage:, auth_strategy:, location: "/To Be Deleted Soon") - - expect(result).to be_success - end - - context "if folder does not exist" do - it "returns a failure", vcr: "nextcloud/delete_folder_not_found" do - result = described_class.call(storage:, auth_strategy:, location: "/IDoNotExist") - - expect(result).to be_failure - expect(result.error_source) - .to be(Storages::Peripherals::StorageInteraction::Nextcloud::Internal::DeleteEntityCommand) - - result.match( - on_failure: ->(error) { expect(error.code).to eq(:not_found) }, - on_success: ->(response) { fail "Expected failure, got #{response}" } - ) - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/download_link_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/download_link_query_spec.rb deleted file mode 100644 index 349d364f217..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/download_link_query_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::DownloadLinkQuery, :webmock do - using Storages::Peripherals::ServiceResultRefinements - - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - let(:file_link) { create(:file_link, origin_id: "182") } - let(:not_existent_file_link) { create(:file_link, origin_id: "DeathStarNumberThree") } - - subject { described_class.new(storage) } - - describe "#call" do - it "responds with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq file_link]) - end - - context "without outbound request involved" do - context "with nil" do - it "returns an error" do - result = subject.call(auth_strategy:, file_link: nil) - - expect(result).to be_failure - expect(result.error_source).to eq(described_class) - expect(result.result).to eq(:error) - end - end - end - - context "with outbound request successful" do - it "returns a result with a download url", vcr: "nextcloud/download_link_query_success" do - download_link = subject.call(auth_strategy:, file_link:) - - expect(download_link).to be_success - - uri = URI(download_link.result) - expect(uri.host).to eq("nextcloud.local") - expect(uri.path) - .to match(/index.php\/apps\/integration_openproject\/direct\/[0-9a-zA-Z]+\/#{file_link.origin_name}/) - end - - it "returns an error if the file is not found", vcr: "nextcloud/download_link_query_not_found" do - download_link = subject.call(auth_strategy:, file_link: not_existent_file_link) - - expect(download_link).to be_failure - expect(download_link.error_source).to eq(described_class) - expect(download_link.result).to eq(:not_found) - end - end - - context "with outbound request returning 200 and an empty body" do - it "refreshes the token and returns success", vcr: "nextcloud/download_link_query_unauthorized" do - download_link = subject.call(auth_strategy:, file_link:) - - expect(download_link).to be_success - - uri = URI(download_link.result) - expect(uri.host).to eq("nextcloud.local") - expect(uri.path) - .to match(/index.php\/apps\/integration_openproject\/direct\/[0-9a-zA-Z]+\/#{file_link.origin_name}/) - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/file_info_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/file_info_query_spec.rb deleted file mode 100644 index 46b14ed9fac..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/file_info_query_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::FileInfoQuery, :vcr, :webmock do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - it_behaves_like "file_info_query: basic query setup" - - it_behaves_like "file_info_query: validating input data" - - context "with a file id requested", vcr: "nextcloud/file_info_query_success_file" do - let(:file_id) { "267" } - let(:file_info) do - Storages::StorageFileInfo.new( - id: file_id, - status: "ok", - status_code: 200, - name: "android-studio-linux.tar.gz", - size: 982713473, - mime_type: "application/gzip", - created_at: Time.parse("1970-01-01T00:00:00Z"), - last_modified_at: Time.parse("2022-12-01T07:43:36Z"), - owner_name: "admin", - owner_id: "admin", - last_modified_by_name: nil, - last_modified_by_id: nil, - permissions: "RGDNVW", - location: "/My%20files/android-studio-linux.tar.gz" - ) - end - - it_behaves_like "file_info_query: successful file/folder response" - end - - context "with a folder id requested", vcr: "nextcloud/file_info_query_success_folder" do - let(:file_id) { "350" } - let(:file_info) do - Storages::StorageFileInfo.new( - id: file_id, - status: "ok", - status_code: 200, - name: "Ümlæûts", - size: 19720, - mime_type: "application/x-op-directory", - created_at: Time.parse("1970-01-01T00:00:00Z"), - last_modified_at: Time.parse("2024-04-29T09:21:03Z"), - owner_name: "admin", - owner_id: "admin", - last_modified_by_name: nil, - last_modified_by_id: nil, - permissions: "RGDNVCK", - location: "/Folder/%C3%9Cml%C3%A6%C3%BBts" - ) - end - - it_behaves_like "file_info_query: successful file/folder response" - end - - context "with a file with special characters in the path", - vcr: "nextcloud/file_info_query_success_special_characters" do - let(:file_id) { "361" } - let(:file_info) do - Storages::StorageFileInfo.new( - id: file_id, - status: "ok", - status_code: 200, - name: "what_have_you_done.md", - size: 0, - mime_type: "text/markdown", - created_at: Time.parse("1970-01-01T00:00:00Z"), - last_modified_at: Time.parse("2024-06-17T09:51:59Z"), - owner_name: "admin", - owner_id: "admin", - last_modified_by_name: nil, - last_modified_by_id: nil, - permissions: "RGDNVW", - location: "/Folder%20with%20spaces/%C3%9Cml%C3%A4uts%20%26%20spe%C2%A2i%C3%A6l%20characters/what_have_you_done.md" - ) - end - - it_behaves_like "file_info_query: successful file/folder response" - end - - context "with a not existing file id", vcr: "nextcloud/file_info_query_not_found" do - let(:file_id) { "not_existent" } - let(:error_source) { described_class } - - it_behaves_like "file_info_query: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/file_path_to_id_map_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/file_path_to_id_map_query_spec.rb deleted file mode 100644 index 781bf308001..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/file_path_to_id_map_query_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::FilePathToIdMapQuery, :webmock do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - it_behaves_like "file_path_to_id_map_query: basic query setup" - - context "with parent folder being root" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/") } - - context "with unset depth (defaults to INFINITY)", vcr: "nextcloud/file_path_to_id_map_query_root_depth_infinite" do - let(:expected_ids) do - { - "/" => "2", - "/Folder with spaces" => "165", - "/Folder with spaces/New Requests" => "166", - "/Folder with spaces/New Requests/I❤️you death star.md" => "167", - "/Folder with spaces/New Requests/request_002.md" => "168", - "/Folder with spaces/Ümläuts & spe¢iæl characters" => "360", - "/Folder with spaces/Ümläuts & spe¢iæl characters/what_have_you_done.md" => "361", - "/My files" => "169", - "/My files/android-studio-linux.tar.gz" => "267", - "/My files/empty" => "172", - "/My files/Ümlæûts" => "350", - "/My files/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "351", - "/Practical_guide_to_BAGGM_Digital.pdf" => "295", - "/Readme.md" => "268", - "/VCR" => "773", - "/VCR/placeholder" => "790" - } - end - - it_behaves_like "file_path_to_id_map_query: successful query" - end - - context "with depth 0", vcr: "nextcloud/file_path_to_id_map_query_root_depth_0" do - let(:depth) { 0 } - let(:expected_ids) { { "/" => "2" } } - - it_behaves_like "file_path_to_id_map_query: successful query" - end - - context "with depth 1", vcr: "nextcloud/file_path_to_id_map_query_root_depth_1" do - let(:depth) { 1 } - let(:expected_ids) do - { - "/" => "2", - "/Folder with spaces" => "165", - "/My files" => "169", - "/Practical_guide_to_BAGGM_Digital.pdf" => "295", - "/Readme.md" => "268", - "/VCR" => "773" - } - end - - it_behaves_like "file_path_to_id_map_query: successful query" - end - end - - context "with a given parent folder", vcr: "nextcloud/file_path_to_id_map_query_parent_folder" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/Folder") } - let(:expected_ids) do - { - "/Folder" => "169", - "/Folder/android-studio-2021.3.1.17-linux.tar.gz" => "267", - "/Folder/empty" => "172", - "/Folder/Ümlæûts" => "350", - "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "351" - } - end - - it_behaves_like "file_path_to_id_map_query: successful query" - end - - context "with not existent parent folder", vcr: "nextcloud/file_path_to_id_map_query_invalid_parent" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") } - let(:error_source) { Storages::Peripherals::StorageInteraction::Nextcloud::Internal::PropfindQuery } - - it_behaves_like "file_path_to_id_map_query: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/files_info_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/files_info_query_spec.rb deleted file mode 100644 index a59792c3466..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/files_info_query_spec.rb +++ /dev/null @@ -1,122 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::FilesInfoQuery, :webmock do - using Storages::Peripherals::ServiceResultRefinements - - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - subject { described_class.new(storage) } - - describe "#call" do - let(:file_ids) { %w[182 203 222] } - - context "without outbound request involved" do - context "with an empty array of file ids" do - it "returns an empty array" do - result = subject.call(auth_strategy:, file_ids: []) - - expect(result).to be_success - expect(result.result).to eq([]) - end - end - - context "with nil" do - it "returns an error" do - result = subject.call(auth_strategy:, file_ids: nil) - - expect(result).to be_failure - expect(result.result).to eq(:error) - end - end - end - - context "with outbound request successful", - vcr: "nextcloud/files_info_query_success" do - context "with an array of file ids" do - it "must return an array of file information when called" do - result = subject.call(auth_strategy:, file_ids:) - expect(result).to be_success - - result.match( - on_success: ->(file_infos) do - expect(file_infos.size).to eq(3) - expect(file_infos).to all(be_a(Storages::StorageFileInfo)) - end, - on_failure: ->(error) { fail "Expected success, got #{error}" } - ) - end - end - end - - context "with outbound request not found" do - context "with a single file id", - vcr: "nextcloud/files_info_query_not_found" do - let(:file_ids) { %w[1234] } - - it "returns an HTTP 200 with individual status code per file ID" do - subject.call(auth_strategy:, file_ids:).match( - on_success: ->(file_infos) do - expect(file_infos.size).to eq(1) - expect(file_infos.first.to_h).to include(status: "Not Found", status_code: 404) - end, - on_failure: ->(error) { fail "Expected success, got #{error}" } - ) - end - end - end - - context "with outbound request not authorized" do - context "with multiple file IDs, one of which is not authorized", - vcr: "nextcloud/files_info_query_only_one_not_authorized" do - let(:file_ids) { %w[182 1234] } - - it "returns an HTTP 200 with individual status code per file ID" do - subject.call(auth_strategy:, file_ids:).match( - on_success: ->(file_infos) do - expect(file_infos.size).to eq(2) - expect(file_infos.map(&:status_code)).to contain_exactly(403, 404) - end, - on_failure: ->(error) { fail "Expected success, got #{error}" } - ) - end - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/files_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/files_query_spec.rb deleted file mode 100644 index f17e214303c..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/files_query_spec.rb +++ /dev/null @@ -1,246 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::FilesQuery, :vcr, :webmock do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, - :as_not_automatically_managed, - oauth_client_token_user: user, - origin_user_id: "m.jade@death.star") - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - it_behaves_like "files_query: basic query setup" - - it_behaves_like "files_query: validating input data" - - context "with parent folder being root", vcr: "nextcloud/files_query_root" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/") } - let(:files_result) do - # FIXME: nextcloud files query currently does not correctly returns modifier and creation date. - Storages::StorageFiles.new( - [ - Storages::StorageFile.new(id: "555", - name: "Folder", - size: 232167, - mime_type: "application/x-op-directory", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/Folder", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "561", - name: "Folder with spaces", - size: 890, - mime_type: "application/x-op-directory", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:52:09Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/Folder%20with%20spaces", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "562", - name: "Ümlæûts", - size: 19720, - mime_type: "application/x-op-directory", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:51:48Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/%c3%9cml%c3%a6%c3%bbts", - permissions: %i[readable writeable]) - ], - Storages::StorageFile.new(id: "385", - name: "Root", - size: 252777, - mime_type: "application/x-op-directory", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/", - permissions: %i[readable writeable]), - [] - ) - end - - it_behaves_like "files_query: successful files response" - end - - context "with a given parent folder", vcr: "nextcloud/files_query_parent_folder" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/Folder/Nested Folder") } - let(:files_result) do - Storages::StorageFiles.new( - [ - Storages::StorageFile.new(id: "603", - name: "giphy.gif", - size: 184726, - mime_type: "image/gif", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:53:24Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/Folder/Nested%20Folder/giphy.gif", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "604", - name: "release_meme.jpg", - size: 46264, - mime_type: "image/jpeg", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:53:30Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/Folder/Nested%20Folder/release_meme.jpg", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "602", - name: "todo.txt", - size: 55, - mime_type: "text/plain", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:53:35Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/Folder/Nested%20Folder/todo.txt", - permissions: %i[readable writeable]) - ], - Storages::StorageFile.new(id: "601", - name: "Nested Folder", - size: 231045, - mime_type: "application/x-op-directory", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/Folder/Nested%20Folder", - permissions: %i[readable writeable]), - [ - Storages::StorageFile.new(id: "8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1", - name: "Root", - location: "/"), - Storages::StorageFile.new(id: "0da2f1cf70005eaeb08333802726c2928503d975e4a70cbdd1a28313cded20ae", - name: "Folder", - location: "/Folder") - ] - ) - end - - it_behaves_like "files_query: successful files response" - end - - context "with parent folder being empty", vcr: "nextcloud/files_query_empty_folder" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/Folder with spaces/very empty folder") } - let(:files_result) do - Storages::StorageFiles.new( - [], - Storages::StorageFile.new(id: "571", - name: "very empty folder", - size: 0, - mime_type: "application/x-op-directory", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:52:04Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/Folder%20with%20spaces/very%20empty%20folder", - permissions: %i[readable writeable]), - [ - Storages::StorageFile.new(id: "8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1", - name: "Root", - location: "/"), - Storages::StorageFile.new(id: "c8776f1f6dd36c023c6615d39f01a71d68dd1707b232115b7a4f58bc6da94e2e", - name: "Folder with spaces", - location: "/Folder%20with%20spaces") - ] - ) - end - - it_behaves_like "files_query: successful files response" - end - - context "with a path full of umlauts", vcr: "nextcloud/files_query_umlauts" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/Ümlæûts") } - let(:files_result) do - Storages::StorageFiles.new( - [ - Storages::StorageFile.new(id: "564", - name: "Anrüchiges deutsches Dokument.docx", - size: 19720, - mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:51:40Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/%c3%9cml%c3%a6%c3%bbts/Anr%c3%bcchiges%20deutsches%20Dokument.docx", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "563", - name: "data", - size: 0, - mime_type: "application/x-op-directory", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:51:30Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/%c3%9cml%c3%a6%c3%bbts/data", - permissions: %i[readable writeable]) - ], - Storages::StorageFile.new(id: "562", - name: "Ümlæûts", - size: 19720, - mime_type: "application/x-op-directory", - created_at: nil, - last_modified_at: Time.zone.parse("2024-08-09T11:51:48Z"), - created_by_name: "Mara Jade", - last_modified_by_name: nil, - location: "/%c3%9cml%c3%a6%c3%bbts", - permissions: %i[readable writeable]), - [ - Storages::StorageFile.new(id: "8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1", - name: "Root", - location: "/") - ] - ) - end - - it_behaves_like "files_query: successful files response" - end - - context "with not existent parent folder", vcr: "nextcloud/files_query_invalid_parent" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") } - let(:error_source) { described_class } - - it_behaves_like "files_query: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/group_users_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/group_users_query_spec.rb deleted file mode 100644 index ab395e98327..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/group_users_query_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::GroupUsersQuery, :webmock do - include NextcloudGroupUserHelper - - let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed, username: "vcr") } - let(:auth_strategy) { Storages::Peripherals::Registry.resolve("nextcloud.authentication.userless").call } - - describe "basic command setup" do - it "is registered as queries.group_users" do - expect(Storages::Peripherals::Registry - .resolve("#{storage}.queries.group_users")).to eq(described_class) - end - - it "responds to #call with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq group]) - end - end - - context "if group exists", vcr: "nextcloud/group_users_success" do - let(:user1) { "m.jade@death.star" } - let(:user2) { "d.vader@death.star" } - let(:user3) { "l.organa@filthy.rebels" } - let(:group) { "Sith Assassins" } - - before do - create_group(auth, storage, group) - add_user_to_group(user1, group) - add_user_to_group(user2, group) - end - - after do - remove_group(auth, storage, group) - end - - it "returns a success" do - result = described_class.call(storage:, auth_strategy:, group:) - expect(result).to be_success - expect(result.result).to include(user1, user2) - expect(result.result).not_to include(user3) - end - end - - context "if group does not exist", vcr: "nextcloud/group_users_not_existing_group" do - let(:user) { "m.jade@death.star" } - let(:group) { "Sith Assassins" } - - it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, group:) - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(:group_does_not_exist) - expect(error.data.source).to eq(described_class) - end - end - - private - - def auth = Storages::Peripherals::StorageInteraction::Authentication[auth_strategy] - - def add_user_to_group(user, group) - Storages::Peripherals::StorageInteraction::Nextcloud::AddUserToGroupCommand - .call(storage:, auth_strategy:, user:, group:) - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/open_file_link_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/open_file_link_query_spec.rb deleted file mode 100644 index 114e2a46936..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/open_file_link_query_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::OpenFileLinkQuery do - let(:storage) { create(:nextcloud_storage, host: "https://example.com") } - let(:file_id) { "1337" } - let(:auth_strategy) { Storages::Peripherals::StorageInteraction::AuthenticationStrategies::BasicAuth.strategy } - - it "responds to .call" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq file_id], - %i[key open_location]) - end - - it "returns the url for opening the file on storage" do - url = described_class.call(storage:, auth_strategy:, file_id:).result - expect(url).to eq("#{storage.host}/index.php/f/#{file_id}?openfile=1") - end - - it "returns the url for opening the file's location on storage" do - url = described_class.call(storage:, auth_strategy:, file_id:, open_location: true).result - expect(url).to eq("#{storage.host}/index.php/f/#{file_id}?openfile=0") - end - - context "with a storage with host url with a sub path" do - let(:storage) { create(:nextcloud_storage, host: "https://example.com/html") } - - it "returns the url for opening the file on storage" do - url = described_class.call(storage:, auth_strategy:, file_id:).result - expect(url).to eq("#{storage.host}/index.php/f/#{file_id}?openfile=1") - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/open_storage_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/open_storage_query_spec.rb deleted file mode 100644 index a05f44612e4..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/open_storage_query_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::OpenStorageQuery do - let(:storage) { create(:nextcloud_storage, host: "https://example.com") } - let(:auth_strategy) { Storages::Peripherals::StorageInteraction::AuthenticationStrategies::BasicAuth.strategy } - - it "responds to .call" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy]) - end - - it "returns the url for opening the file on storage" do - url = described_class.call(storage:, auth_strategy:).result - expect(url).to eq("#{storage.host}/index.php/apps/files") - end - - context "with a storage with host url with a sub path" do - let(:storage) { create(:nextcloud_storage, host: "https://example.com/html") } - - it "returns the url for opening the file on storage" do - url = described_class.call(storage:, auth_strategy:).result - expect(url).to eq("#{storage.host}/index.php/apps/files") - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command_spec.rb deleted file mode 100644 index 498de7cca27..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/remove_user_from_group_command_spec.rb +++ /dev/null @@ -1,153 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::RemoveUserFromGroupCommand, :webmock do - include NextcloudGroupUserHelper - - let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed, username: "vcr") } - let(:auth_strategy) { Storages::Peripherals::Registry.resolve("nextcloud.authentication.userless").call } - - describe "basic command setup" do - it "is registered as commands.remove_user_from_group" do - expect(Storages::Peripherals::Registry - .resolve("#{storage}.commands.remove_user_from_group")).to eq(described_class) - end - - it "responds to #call with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq user], - %i[keyreq group]) - end - end - - shared_examples_for "failing request" do |error_code:| - it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, user:, group:) - expect(result).to be_failure - - error = result.errors - expect(error.code).to eq(error_code) - expect(error.data.source).to eq(described_class) - end - end - - context "if user exists in target group", vcr: "nextcloud/remove_user_from_group_success" do - let(:user) { "m.jade@death.star" } - let(:group) { "Sith Assassins" } - - before do - create_group(auth, storage, group) - add_user_to_group(user, group) - end - - after do - remove_group(auth, storage, group) - end - - it "returns a success" do - members = group_members(group) - expect(members).to include(user) - - result = described_class.call(storage:, auth_strategy:, user:, group:) - expect(result).to be_success - - members = group_members(group) - expect(members).not_to include(user) - end - end - - context "if user does not exist in target group", vcr: "nextcloud/remove_user_from_group_no_user" do - let(:user) { "m.jade@death.star" } - let(:group) { "Sith Assassins" } - - before do - create_group(auth, storage, group) - end - - after do - remove_group(auth, storage, group) - end - - it "returns a success" do - members = group_members(group) - expect(members).not_to include(user) - - result = described_class.call(storage:, auth_strategy:, user:, group:) - expect(result).to be_success - - members = group_members(group) - expect(members).not_to include(user) - end - end - - context "if target group does not exist", vcr: "nextcloud/remove_user_from_group_not_existing_group" do - let(:user) { "m.jade@death.star" } - let(:group) { "Sith Assassins" } - - it_behaves_like "failing request", error_code: :group_does_not_exist - end - - context "if user does not exist", vcr: "nextcloud/remove_user_from_group_not_existing_user" do - let(:user) { "this is not the user you are looking for" } - let(:group) { "Sith Assassins" } - - before do - create_group(auth, storage, group) - end - - after do - remove_group(auth, storage, group) - end - - it_behaves_like "failing request", error_code: :user_does_not_exist - end - - private - - def auth = Storages::Peripherals::StorageInteraction::Authentication[auth_strategy] - - def add_user_to_group(user, group) - Storages::Peripherals::StorageInteraction::Nextcloud::AddUserToGroupCommand - .call(storage:, auth_strategy:, user:, group:) - end - - def group_members(group) - Storages::Peripherals::StorageInteraction::Nextcloud::GroupUsersQuery - .call(storage:, auth_strategy:, group:) - .result - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command_spec.rb deleted file mode 100644 index 22ecd3f447b..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/rename_file_command_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::RenameFileCommand, :webmock do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::Registry.resolve("nextcloud.authentication.user_bound").call(user:, storage:) - end - - it_behaves_like "rename_file_command: basic command setup" - - it_behaves_like "rename_file_command: validating input data" - - context "when renaming a folder", vcr: "nextcloud/rename_file_success" do - let(:file_id) { "169" } - let(:name) { "I am the senat" } - - it_behaves_like "rename_file_command: successful file renaming" - end - - context "when renaming a file inside a subdirectory", vcr: "nextcloud/rename_file_with_location_success" do - let(:file_id) { "167" } - let(:name) { "I❤️you death star.md" } - - it_behaves_like "rename_file_command: successful file renaming" - end - - context "when trying to rename a not existent file", vcr: "nextcloud/rename_file_not_found" do - let(:file_id) { "sith_have_yellow_light_sabers" } - let(:name) { "this_will_not_happen.txt" } - let(:error_source) { Storages::Peripherals::StorageInteraction::Nextcloud::FileInfoQuery } - - it_behaves_like "rename_file_command: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/set_permissions_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/set_permissions_command_spec.rb deleted file mode 100644 index dc04ea1a818..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/set_permissions_command_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::SetPermissionsCommand, :webmock do - let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed, username: "vcr") } - let(:auth_strategy) { Storages::Peripherals::Registry.resolve("nextcloud.authentication.userless").call } - - let(:test_folder) do - Storages::Peripherals::Registry - .resolve("nextcloud.commands.create_folder") - .call(storage:, - auth_strategy:, - folder_name: "Permission Test Folder", - parent_location: Storages::Peripherals::ParentFolder.new("/VCR")) - .result - end - - it_behaves_like "set_permissions_command: basic command setup" - - context "if folder does not exists", vcr: "nextcloud/set_permissions_not_found_folder" do - let(:error_source) { Storages::Peripherals::StorageInteraction::Nextcloud::FileInfoQuery } - let(:input_data) { permission_input_data("1337", []) } - - it_behaves_like "set_permissions_command: not found" - end - - context "if no permissions exist", vcr: "nextcloud/set_permissions_new" do - let(:user_permissions) do - [ - { user_id: "m.jade@death.star", permissions: %i[read_files write_files] }, - { user_id: "admin", permissions: %i[read_files write_files create_files delete_files] } - ] - end - - it_behaves_like "set_permissions_command: creates new permissions" - end - - context "if a permission set already exist", vcr: "nextcloud/set_permissions_replacing_permissions" do - let(:previous_permissions) do - [{ user_id: "admin", permissions: %i[read_files write_files create_files delete_files] }] - end - let(:replacing_permissions) do - [{ user_id: "m.jade@death.star", permissions: %i[read_files write_files] }] - end - - it_behaves_like "set_permissions_command: replaces already set permissions" - end - - context "if a user does not exist", - skip: "When setting permissions for a user that does not exists, nextcloud's response doesn't contain the " \ - "needed information. We need to work around this by maybe having a separate request fetching ACLs " \ - "after setting them.", - vcr: "nextcloud/set_permissions_invalid_user_id" do - let(:user_permissions) do - [{ user_id: "luke_the_sky", permissions: %i[read_files write_files create_files delete_files share_files] }] - end - - it_behaves_like "set_permissions_command: unknown remote identity" - end - - private - - def permission_input_data(file_id, user_permissions) - Storages::Peripherals::StorageInteraction::Inputs::SetPermissions.build(file_id:, user_permissions:).value! - end - - def current_remote_permissions - Storages::Peripherals::StorageInteraction::Authentication[auth_strategy].call(storage:) do |http| - request_url = Storages::UrlBuilder.url(storage.uri, - "remote.php/dav/files", - storage.username, - test_folder.location) - response = http.request(:propfind, request_url, xml: permission_request_body) - parse_acl_xml response.body.to_s - end - end - - def permission_request_body - Nokogiri::XML::Builder.new do |xml| - xml["d"].propfind( - "xmlns:d" => "DAV:", - "xmlns:nc" => "http://nextcloud.org/ns" - ) do - xml["d"].prop do - xml["nc"].send(:"acl-list") - end - end - end.to_xml - end - - def parse_acl_xml(xml) - found_code = "d:status[text() = 'HTTP/1.1 200 OK']" - not_found_code = "d:status[text() = 'HTTP/1.1 404 Not Found']" - happy_path = "/d:multistatus/d:response/d:propstat[#{found_code}]/d:prop/nc:acl-list" - not_found_path = "/d:multistatus/d:response/d:propstat[#{not_found_code}]/d:prop" - - if Nokogiri::XML(xml).xpath(not_found_path).children.map(&:name).include?("acl-list") - [] - else - Nokogiri::XML(xml).xpath(happy_path).children.map do |acl| - acl.children.each_with_object({ user_id: "", permissions: [] }) do |entry, agg| - agg[:user_id] = entry.text if entry.name == "acl-mapping-id" - agg[:permissions] = translate_mask_to_permissions(entry.text.to_i) if entry.name == "acl-permissions" - end - end - end - end - - def translate_mask_to_permissions(number) - described_class::PERMISSIONS_MAP.each_with_object([]) do |(permission, mask), list| - list << permission if number & mask == mask - end - end - - # TODO: Delete folder for nextcloud still works on a location, not a file id. - def clean_up(_file_id) - Storages::Peripherals::Registry - .resolve("nextcloud.commands.delete_folder") - .call(storage:, auth_strategy:, location: test_folder.location) - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/upload_link_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/upload_link_query_spec.rb deleted file mode 100644 index 4556fdc9d97..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/upload_link_query_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::UploadLinkQuery, :webmock do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, oauth_client_token_user: user) - end - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - it_behaves_like "upload_link_query: basic query setup" - - context "when requesting an upload link for an existing file", vcr: "nextcloud/upload_link_success" do - let(:upload_data) do - Storages::Peripherals::StorageInteraction::Inputs::UploadData - .build(folder_id: "169", file_name: "DeathStart_blueprints.tiff").value! - end - let(:token) { "SrQJeC5zM3B5Gw64d7dEQFQpFw8YBAtZWoxeLb59AR7PpGPyoGAkAko5G6ZiZ2HA" } - let(:upload_url) { "https://nextcloud.local/index.php/apps/integration_openproject/direct-upload/#{token}" } - let(:upload_method) { :post } - - it_behaves_like "upload_link_query: successful upload link response" - end - - context "when requesting an upload link for a not existing file", vcr: "nextcloud/upload_link_not_found" do - let(:upload_data) do - Storages::Peripherals::StorageInteraction::Inputs::UploadData - .build(folder_id: "1337", file_name: "DeathStart_blueprints.tiff").value! - end - let(:error_source) { described_class } - - it_behaves_like "upload_link_query: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/user_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/user_query_spec.rb deleted file mode 100644 index 0261cc0a0c1..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/user_query_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::Nextcloud::UserQuery, :webmock do - let(:user) { create(:user) } - let(:storage) do - create(:nextcloud_storage_with_local_connection, - :as_automatically_managed, - username: "vcr", - oauth_client_token_user: user) - end - let(:userless_strategy) { Storages::Peripherals::Registry.resolve("nextcloud.authentication.userless").call } - let(:user_bound_strategy) do - Storages::Peripherals::Registry.resolve("nextcloud.authentication.user_bound").call(user:, storage:) - end - - it "is registered" do - expect(Storages::Peripherals::Registry - .resolve("#{storage}.queries.user")).to eq(described_class) - end - - it "responds to #call with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy]) - end - - it "responds with failure with invalid token", vcr: "nextcloud/user_query_unauthorized" do - result = described_class.call(storage:, auth_strategy: userless_strategy) - - expect(result).to be_failure - expect(result.result).to eq(:unauthorized) - end - - it "responds with success with valid token", vcr: "nextcloud/user_query_success" do - result = described_class.call(storage:, auth_strategy: user_bound_strategy) - - expect(result).to be_success - expect(result.result).to eq(id: "admin") - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/util_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/util_spec.rb deleted file mode 100644 index f60e7348d77..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/nextcloud/util_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe(Storages::Peripherals::StorageInteraction::Nextcloud::Util) do - describe ".origin_user_id" do - it "responds with appropriate origin_user_id when user has two remote identities" \ - "for one integration within two different auth_sources" do - oauth_client = create(:oauth_client) - oidc_provider = create(:oidc_provider) - integration = create(:nextcloud_storage, oauth_client:) - user = create(:user, authentication_provider: oidc_provider) - create(:remote_identity, user:, auth_source: oauth_client, integration:, origin_user_id: "456") - create(:remote_identity, user:, auth_source: oidc_provider, integration:, origin_user_id: "123") - sso_strategy = Storages::Peripherals::StorageInteraction::AuthenticationStrategies::SsoUserToken - .strategy - .with_user(user) - oauth_strategy = Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken - .strategy - .with_user(user) - - expect(described_class.origin_user_id(caller: self.class, storage: integration, - auth_strategy: sso_strategy).result).to eq("123") - expect(described_class.origin_user_id(caller: self.class, storage: integration, - auth_strategy: oauth_strategy).result).to eq("456") - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/copy_template_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/copy_template_folder_command_spec.rb deleted file mode 100644 index 10d74cdf3da..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/copy_template_folder_command_spec.rb +++ /dev/null @@ -1,194 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CopyTemplateFolderCommand, :webmock do - shared_let(:storage) { create(:sharepoint_dev_drive_storage) } - - shared_let(:original_folders) do - use_storages_vcr_cassette("one_drive/copy_template_folder_existing_folders") { existing_folder_tuples } - end - - shared_let(:base_template_folder) do - use_storages_vcr_cassette("one_drive/copy_template_folder_base_folder") { create_base_folder } - end - - shared_let(:source_path) { base_template_folder.id } - - it "is registered under commands.one_drive.copy_template_folder" do - expect(Storages::Peripherals::Registry.resolve("one_drive.commands.copy_template_folder")).to eq(described_class) - end - - it "responds to .call" do - expect(described_class).to respond_to(:call) - end - - it ".call takes 3 required parameters: storage, source_path, destination_path" do - method = described_class.method(:call) - - expect(method.parameters) - .to contain_exactly(%i[keyreq auth_strategy], %i[keyreq storage], %i[keyreq source_path], %i[keyreq destination_path]) - end - - it "destination_path and source_path can't be empty" do - missing_source = described_class.call(auth_strategy:, storage:, source_path: "", destination_path: "Path") - missing_path = described_class.call(auth_strategy:, storage:, source_path: "Path", destination_path: nil) - missing_both = described_class.call(auth_strategy:, storage:, source_path: nil, destination_path: "") - - expect([missing_both, missing_path, missing_source]).to all(be_failure) - end - - describe "#call" do - # rubocop:disable RSpec/BeforeAfterAll - before(:all) do - use_storages_vcr_cassette("one_drive/copy_template_folder_setup") { setup_template_folder } - end - - after(:all) do - use_storages_vcr_cassette("one_drive/copy_template_folder_teardown") { delete_template_folder } - end - # rubocop:enable RSpec/BeforeAfterAll - - it "copies origin folder and all underlying files and folders to the destination_path", - vcr: "one_drive/copy_template_folder_copy_successful" do - command_result = described_class.call(auth_strategy:, storage:, source_path:, destination_path: "My New Folder") - - expect(command_result).to be_success - expect(command_result.result).to be_requires_polling - expect(command_result.result.polling_url).to match %r - ensure - delete_copied_folder(command_result.result.polling_url) - end - - describe "error handling" do - context "when the source_path does not exist" do - it "fails", vcr: "one_drive/copy_template_source_not_found" do - result = described_class.call(auth_strategy:, storage:, source_path: "TheCakeIsALie", destination_path: "Not Happening") - - expect(result).to be_failure - end - - it "explains the nature of the error", vcr: "one_drive/copy_template_source_not_found" do - result = described_class.call(auth_strategy:, storage:, source_path: "TheCakeIsALie", destination_path: "Not Happening") - - expect(result.errors.to_s).to match(/not_found \| Template folder not found/) - end - end - - context "when it would overwrite an already existing folder" do - it "fails", vcr: "one_drive/copy_template_folder_no_overwrite" do - existing_folder = original_folders.first[:name] - result = described_class.call(auth_strategy:, storage:, source_path:, destination_path: existing_folder) - - expect(result).to be_failure - end - - it "explains the nature of the error", vcr: "one_drive/copy_template_folder_no_overwrite" do - existing_folder = original_folders.first[:name] - result = described_class.call(auth_strategy:, storage:, source_path:, destination_path: existing_folder) - - expect(result.errors.to_s).to match(/conflict \| The copy would overwrite an already existing folder/) - end - end - end - end - - private - - def create_base_folder - Storages::Peripherals::Registry - .resolve("one_drive.commands.create_folder") - .call(storage:, - auth_strategy:, - folder_name: "Test Template Folder", - parent_location: Storages::Peripherals::ParentFolder.new("/")) - .result - end - - def setup_template_folder - raise if source_path.nil? - - parent_location = Storages::Peripherals::ParentFolder.new(source_path) - - command = Storages::Peripherals::Registry - .resolve("one_drive.commands.create_folder").new(storage) - command.call(auth_strategy:, folder_name: "Empty Subfolder", parent_location:) - - subfolder = command.call(auth_strategy:, folder_name: "Subfolder with File", parent_location:).result - file_name = "files_query_root.yml" - upload_data = Storages::Peripherals::StorageInteraction::Inputs::UploadData.build(folder_id: subfolder.id, file_name:).value! - upload_link = Storages::Peripherals::Registry - .resolve("one_drive.queries.upload_link") - .call(storage:, auth_strategy:, upload_data:) - .result - - path = Rails.root.join("modules/storages/spec/support/fixtures/vcr_cassettes/one_drive", file_name) - File.open(path, "rb") do |file_handle| - HTTPX.with(headers: { content_length: file_handle.size, - "Content-Range" => "bytes 0-#{file_handle.size - 1}/#{file_handle.size}" }) - .put(upload_link.destination, body: file_handle.read).raise_for_status - end - end - - def delete_template_folder - Storages::Peripherals::Registry - .resolve("one_drive.commands.delete_folder") - .call(storage:, auth_strategy:, location: base_template_folder.id) - end - - def existing_folder_tuples - Storages::Peripherals::StorageInteraction::Authentication[auth_strategy].call(storage:) do |http| - url = Storages::UrlBuilder.url(storage.uri, "/v1.0/drives", storage.drive_id, "/root/children") - response = http.get("#{url}?$select=name,id,folder") - - response.json(symbolize_keys: true).fetch(:value, []).filter_map do |item| - next unless item.key?(:folder) - - item.slice(:name, :id) - end - end - end - - def delete_copied_folder(url) - extractor_regex = /.+\/items\/(?\w+)\?/ - match_data = extractor_regex.match(url) - location = match_data[:item_id] - - Storages::Peripherals::Registry - .resolve("one_drive.commands.delete_folder") - .call(storage:, auth_strategy:, location:) - end - - def auth_strategy - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb deleted file mode 100644 index 7d4e348d82f..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/create_folder_command_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::CreateFolderCommand, :webmock do - let(:storage) { create(:sharepoint_dev_drive_storage) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy - end - - it_behaves_like "create_folder_command: basic command setup" - - context "when creating a folder in the root", vcr: "one_drive/create_folder_root" do - let(:folder_name) { "Földer CreatedBy Çommand" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } - let(:path) { "/F%C3%B6lder%20CreatedBy%20%C3%87ommand" } - - it_behaves_like "create_folder_command: successful folder creation" - end - - context "when creating a folder in a parent folder", vcr: "one_drive/create_folder_parent" do - let(:folder_name) { "Földer CreatedBy Çommand" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU") } - let(:path) { "/Folder%20with%20spaces/F%C3%B6lder%20CreatedBy%20%C3%87ommand" } - - it_behaves_like "create_folder_command: successful folder creation" - end - - context "when creating a folder in a non-existing parent folder", vcr: "one_drive/create_folder_parent_not_found" do - let(:folder_name) { "Földer CreatedBy Çommand" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("01AZJL5PKU2WV3U3RKKFF4A7ZCWVBXRTEU") } - let(:error_source) { described_class } - - it_behaves_like "create_folder_command: parent not found" - end - - context "when folder already exists", vcr: "one_drive/create_folder_already_exists" do - let(:folder_name) { "Folder" } - let(:parent_location) { Storages::Peripherals::ParentFolder.new("/") } - let(:error_source) { described_class } - - it_behaves_like "create_folder_command: folder already exists" - end - - private - - def delete_created_folder(folder) - Storages::Peripherals::Registry - .resolve("one_drive.commands.delete_folder") - .call(storage:, auth_strategy:, location: folder.id) - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/delete_folder_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/delete_folder_command_spec.rb deleted file mode 100644 index 864edbd7539..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/delete_folder_command_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::DeleteFolderCommand, :vcr, :webmock do - let(:storage) { create(:sharepoint_dev_drive_storage) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy - end - - it "is registered as commands.one_drive.delete_folder" do - expect(Storages::Peripherals::Registry.resolve("one_drive.commands.delete_folder")).to eq(described_class) - end - - it ".call requires storage and location as keyword arguments" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq location]) - end - - it "deletes a folder", vcr: "one_drive/delete_folder" do - parent_location = Storages::Peripherals::ParentFolder.new("/") - - create_result = Storages::Peripherals::Registry - .resolve("one_drive.commands.create_folder") - .call(storage:, auth_strategy:, folder_name: "To Be Deleted Soon", parent_location:) - - folder = create_result.result - - expect(described_class.call(storage:, auth_strategy:, location: folder.id)).to be_success - end - - it "when the folder is not found, returns a failure", vcr: "one_drive/delete_folder_not_found" do - result = described_class.call(storage:, auth_strategy:, location: "NOT_HERE") - expect(result).to be_failure - expect(result.result).to eq(:not_found) - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/download_link_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/download_link_query_spec.rb deleted file mode 100644 index 48fd002c807..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/download_link_query_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::DownloadLinkQuery, :vcr, :webmock do - using Storages::Peripherals::ServiceResultRefinements - - let(:user) { create(:user) } - let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - let(:file_link) { create(:file_link, origin_id: "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE") } - let(:not_existent_file_link) { create(:file_link, origin_id: "DeathStarNumberThree") } - - subject { described_class.new(storage) } - - describe "#call" do - it "responds with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq file_link]) - end - - context "without outbound request involved" do - context "with nil" do - it "returns an error" do - result = subject.call(auth_strategy:, file_link: nil) - - expect(result).to be_failure - expect(result.error_source).to eq(described_class) - expect(result.result).to eq(:error) - end - end - end - - context "with outbound request successful" do - it "returns a result with a download url", vcr: "one_drive/download_link_query_success" do - download_link = subject.call(auth_strategy:, file_link:) - - expect(download_link).to be_success - - uri = URI(download_link.result) - expect(uri.host).to eq("finn.sharepoint.com") - expect(uri.path).to eq("/sites/openprojectfilestoragetests/_layouts/15/download.aspx") - end - - it "returns an error if the file is not found", vcr: "one_drive/download_link_query_not_found" do - download_link = subject.call(auth_strategy:, file_link: not_existent_file_link) - - expect(download_link).to be_failure - expect(download_link.error_source).to eq(described_class) - expect(download_link.result).to eq(:not_found) - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_info_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_info_query_spec.rb deleted file mode 100644 index c320fbde53d..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_info_query_spec.rb +++ /dev/null @@ -1,125 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::FileInfoQuery, :vcr, :webmock do - let(:user) { create(:user) } - let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - - let(:auth_strategy) do - Storages::Peripherals::Registry["one_drive.authentication.user_bound"].call(user:, storage:) - end - - it_behaves_like "file_info_query: basic query setup" - - it_behaves_like "file_info_query: validating input data" - - context "with a file id requested", vcr: "one_drive/file_info_query_success_file" do - let(:file_id) { "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA" } - let(:file_info) do - Storages::StorageFileInfo.new( - id: file_id, - status: "ok", - status_code: 200, - name: "NextcloudHub.md", - size: 1095, - mime_type: "application/octet-stream", - created_at: Time.parse("2023-09-26T14:45:25Z"), - last_modified_at: Time.parse("2023-09-26T14:46:13Z"), - owner_name: "Eric Schubert", - owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - last_modified_by_name: "Eric Schubert", - last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - permissions: nil, - location: "/Folder/Subfolder/NextcloudHub.md" - ) - end - - it_behaves_like "file_info_query: successful file/folder response" - end - - context "with a folder id requested", vcr: "one_drive/file_info_query_success_folder" do - let(:file_id) { "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB" } - let(:file_info) do - Storages::StorageFileInfo.new( - id: file_id, - status: "ok", - status_code: 200, - name: "Ümlæûts", - size: 20789, - mime_type: "application/x-op-directory", - created_at: Time.parse("2023-10-09T15:26:32Z"), - last_modified_at: Time.parse("2023-10-09T15:26:32Z"), - owner_name: "Eric Schubert", - owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - last_modified_by_name: "Eric Schubert", - last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - permissions: nil, - location: "/Folder/%C3%9Cml%C3%A6%C3%BBts" - ) - end - - it_behaves_like "file_info_query: successful file/folder response" - end - - context "with a file with special characters in the path", - vcr: "one_drive/file_info_query_success_special_characters" do - let(:file_id) { "01AZJL5PITB4FWUTEDCZGLV3WXG5TJX5A2" } - let(:file_info) do - Storages::StorageFileInfo.new( - id: file_id, - status: "ok", - status_code: 200, - name: "what_have_you_done.png", - size: 226985, - mime_type: "image/png", - created_at: Time.parse("2024-06-17T09:37:58Z"), - last_modified_at: Time.parse("2024-06-17T09:38:15Z"), - owner_name: "Eric Schubert", - owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - last_modified_by_name: "Eric Schubert", - last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - permissions: nil, - location: "/Folder%20with%20spaces/%C3%9Cml%C3%A4uts%20%26%20spe%C2%A2i%C3%A6l%20characters/what_have_you_done.png" - ) - end - - it_behaves_like "file_info_query: successful file/folder response" - end - - context "with a not existing file id", vcr: "one_drive/file_info_query_not_found" do - let(:file_id) { "not_existent" } - let(:error_source) { Storages::Peripherals::StorageInteraction::OneDrive::Internal::DriveItemQuery } - - it_behaves_like "file_info_query: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_path_to_id_map_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_path_to_id_map_query_spec.rb deleted file mode 100644 index b6a157ab4a7..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/file_path_to_id_map_query_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::FilePathToIdMapQuery, :webmock do - let(:storage) { create(:sharepoint_dev_drive_storage) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy - end - - it_behaves_like "file_path_to_id_map_query: basic query setup" - - context "with parent folder being root", vcr: "one_drive/file_path_to_id_map_query_root" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/") } - - context "with unset depth (defaults to INFINITY)" do - let(:expected_ids) do - { - "/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", - "/Folder with spaces" => "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", - "/Folder with spaces/very empty folder" => "01AZJL5PMGEIRPHZPHRRH2NM3D734VIR7H", - "/Folder with spaces/wordle1.png" => "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN", - "/Folder with spaces/wordle2.png" => "01AZJL5PIIFUD6A765KBAIAEMYACAFB2WP", - "/Folder with spaces/wordle3.png" => "01AZJL5PL4AUJEU43CQZFJKN7BQPRP3BLF", - "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", - "/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI", - "/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", - "/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", - "/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", - "/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS", - "/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY", - "/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46", - "/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", - "/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", - "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE", - "/Permissions Folder" => "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB" - } - end - - it_behaves_like "file_path_to_id_map_query: successful query" - end - - context "with a depth of 0" do - let(:depth) { 0 } - let(:expected_ids) { { "/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ" } } - - it_behaves_like "file_path_to_id_map_query: successful query" - end - - context "with a depth of 1" do - let(:depth) { 1 } - let(:expected_ids) do - { - "/" => "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", - "/Folder with spaces" => "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", - "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", - "/Permissions Folder" => "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB" - } - end - - it_behaves_like "file_path_to_id_map_query: successful query" - end - end - - context "with a given parent folder", vcr: "one_drive/file_path_to_id_map_query_parent_folder" do - let(:folder) { Storages::Peripherals::ParentFolder.new("01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR") } - let(:expected_ids) do - { - "/Folder" => "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", - "/Folder/Images" => "01AZJL5PMIF7ND3KH6FVDLZYP3E36ERFGI", - "/Folder/Subfolder" => "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", - "/Folder/Ümlæûts" => "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", - "/Folder/Document.docx" => "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", - "/Folder/Sheet.xlsx" => "01AZJL5PLB7SH7633RMBHIH6KVMQRU4RJS", - "/Folder/Images/der_laufende.jpeg" => "01AZJL5PLZFCARRQIDFJF36UL2WTLXTNSY", - "/Folder/Images/written_in_stone.webp" => "01AZJL5PLNCKWYI752YBHYYJ6RBFZWOZ46", - "/Folder/Subfolder/NextcloudHub.md" => "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", - "/Folder/Subfolder/test.txt" => "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", - "/Folder/Ümlæûts/Anrüchiges deutsches Dokument.docx" => "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE" - } - end - - it_behaves_like "file_path_to_id_map_query: successful query" - end - - context "with not existent parent folder", vcr: "one_drive/file_path_to_id_map_query_invalid_parent" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") } - let(:error_source) { Storages::Peripherals::StorageInteraction::OneDrive::Internal::DriveItemQuery } - - it_behaves_like "file_path_to_id_map_query: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/files_info_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/files_info_query_spec.rb deleted file mode 100644 index 0c1f0e42fc7..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/files_info_query_spec.rb +++ /dev/null @@ -1,173 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::FilesInfoQuery, :vcr, :webmock do - using Storages::Peripherals::ServiceResultRefinements - - let(:user) { create(:user) } - let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - subject(:query) { described_class.new(storage) } - - describe "#call" do - it "responds with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[key file_ids]) - end - - context "without outbound request involved" do - context "with an empty array of file ids" do - it "returns an empty array" do - result = query.call(auth_strategy:, file_ids: []) - - expect(result).to be_success - expect(result.result).to eq([]) - end - end - - context "with nil" do - it "returns an error" do - result = query.call(auth_strategy:, file_ids: nil) - - expect(result).to be_failure - expect(result.result).to eq(:error) - end - end - end - - context "with outbound requests successful", vcr: "one_drive/files_info_query_success" do - context "with an array of file ids" do - let(:file_ids) do - %w( - 01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU - 01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU - 01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA - ) - end - - # rubocop:disable RSpec/ExampleLength - it "must return an array of file information when called" do - result = query.call(auth_strategy:, file_ids:) - expect(result).to be_success - - result.match( - on_success: ->(file_infos) do - expect(file_infos.size).to eq(3) - expect(file_infos).to all(be_a(Storages::StorageFileInfo)) - expect(file_infos.map(&:to_h)) - .to eq([ - { - status: "ok", - status_code: 200, - id: "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", - name: "Folder with spaces", - size: 35141, - mime_type: "application/x-op-directory", - created_at: Time.parse("2023-09-26T14:38:57Z"), - last_modified_at: Time.parse("2023-09-26T14:38:57Z"), - owner_name: "Eric Schubert", - owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - last_modified_by_name: "Eric Schubert", - last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - permissions: nil, - location: "/Folder%20with%20spaces" - }, - { - status: "ok", - status_code: 200, - id: "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU", - name: "Document.docx", - size: 22514, - mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - created_at: Time.parse("2023-09-26T14:40:58Z"), - last_modified_at: Time.parse("2023-09-26T14:42:03Z"), - owner_name: "Eric Schubert", - owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - last_modified_by_name: "Eric Schubert", - last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - permissions: nil, - location: "/Folder/Document.docx" - }, - { - status: "ok", - status_code: 200, - id: "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", - name: "NextcloudHub.md", - size: 1095, - mime_type: "application/octet-stream", - created_at: Time.parse("2023-09-26T14:45:25Z"), - last_modified_at: Time.parse("2023-09-26T14:46:13Z"), - owner_name: "Eric Schubert", - owner_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - last_modified_by_name: "Eric Schubert", - last_modified_by_id: "0a0d38a9-a59b-4245-93fa-0d2cf727f17a", - permissions: nil, - location: "/Folder/Subfolder/NextcloudHub.md" - } - ]) - end, - on_failure: ->(error) { fail "Expected success, got #{error}" } - ) - end - # rubocop:enable RSpec/ExampleLength - end - end - - context "with one outbound request returning not found", vcr: "one_drive/files_info_query_one_not_found" do - context "with an array of file ids" do - let(:file_ids) { %w[01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU not_existent] } - - it "must return an array of file information when called" do - result = query.call(auth_strategy:, file_ids:) - expect(result).to be_success - - result.match( - on_success: ->(file_infos) do - expect(file_infos.size).to eq(2) - expect(file_infos).to all(be_a(Storages::StorageFileInfo)) - expect(file_infos[1].id).to eq("not_existent") - expect(file_infos[1].status).to eq("itemNotFound") - expect(file_infos[1].status_code).to eq(404) - end, - on_failure: ->(error) { fail "Expected success, got #{error}" } - ) - end - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/files_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/files_query_spec.rb deleted file mode 100644 index 23b44aaf0d3..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/files_query_spec.rb +++ /dev/null @@ -1,202 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::FilesQuery, :webmock do - let(:user) { create(:user) } - let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthUserToken.strategy.with_user(user) - end - - it_behaves_like "files_query: basic query setup" - - it_behaves_like "files_query: validating input data" - - context "with parent folder being root", vcr: "one_drive/files_query_root" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/") } - let(:files_result) do - Storages::StorageFiles.new( - [ - Storages::StorageFile.new(id: "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR", - name: "Folder", - size: 260500, - mime_type: "application/x-op-directory", - created_at: Time.zone.parse("2023-09-26T14:38:50Z"), - last_modified_at: Time.zone.parse("2023-09-26T14:38:50Z"), - created_by_name: "Eric Schubert", - last_modified_by_name: "Eric Schubert", - location: "/Folder", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "01AZJL5PKU2WV3U3RKKFF2A7ZCWVBXRTEU", - name: "Folder with spaces", - size: 35141, - mime_type: "application/x-op-directory", - created_at: Time.zone.parse("2023-09-26T14:38:57Z"), - last_modified_at: Time.zone.parse("2023-09-26T14:38:57Z"), - created_by_name: "Eric Schubert", - last_modified_by_name: "Eric Schubert", - location: "/Folder%20with%20spaces", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "01AZJL5PN3LVLHH2RSZZDJ6ZFAD3OWSGYB", - name: "Permissions Folder", - size: 0, - mime_type: "application/x-op-directory", - created_at: Time.zone.parse("2024-01-12T09:05:10Z"), - last_modified_at: Time.zone.parse("2024-01-12T09:05:24Z"), - created_by_name: "Marcello Rocha", - last_modified_by_name: "Marcello Rocha", - location: "/Permissions%20Folder", - permissions: %i[readable writeable]) - ], - Storages::StorageFile.new(id: "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", - name: "Root", - location: "/", - permissions: %i[readable writeable]), - [] - ) - end - - it_behaves_like "files_query: successful files response" - end - - context "with a given parent folder", vcr: "one_drive/files_query_parent_folder" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/Folder/Subfolder") } - let(:files_result) do - Storages::StorageFiles.new( - [ - Storages::StorageFile.new(id: "01AZJL5PNCQCEBFI3N7JGZSX5AOX32Z3LA", - name: "NextcloudHub.md", - size: 1095, - mime_type: "application/octet-stream", - created_at: Time.zone.parse("2023-09-26T14:45:25Z"), - last_modified_at: Time.zone.parse("2023-09-26T14:46:13Z"), - created_by_name: "Eric Schubert", - last_modified_by_name: "Eric Schubert", - location: "/Folder/Subfolder/NextcloudHub.md", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "01AZJL5PLOL2KZTJNVFBCJWFXYGYVBQVMZ", - name: "test.txt", - size: 28, - mime_type: "text/plain", - created_at: Time.zone.parse("2023-09-26T14:45:23Z"), - last_modified_at: Time.zone.parse("2023-09-26T14:45:45Z"), - created_by_name: "Eric Schubert", - last_modified_by_name: "Eric Schubert", - location: "/Folder/Subfolder/test.txt", - permissions: %i[readable writeable]) - ], - Storages::StorageFile.new(id: "01AZJL5PPWP5UOATNRJJBYJG5TACDHEUAG", - name: "Subfolder", - location: "/Folder/Subfolder", - permissions: %i[readable writeable]), - [ - Storages::StorageFile.new(id: "a1d45ff742d2175c095f0a7173f93fc3fc23664a953ceae6778fe15398818c2d", - name: "Root", - location: "/", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "74ccd43303847f2655300641a934959cdb11689ce171aa0f00faa92917fbd340", - name: "Folder", - location: "/Folder") - ] - ) - end - - it_behaves_like "files_query: successful files response" - end - - context "with parent folder being empty", vcr: "one_drive/files_query_empty_folder" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/Folder with spaces/very empty folder") } - let(:files_result) do - Storages::StorageFiles.new( - [], - Storages::StorageFile.new(id: "01AZJL5PMGEIRPHZPHRRH2NM3D734VIR7H", - name: "very empty folder", - location: "/Folder%20with%20spaces/very%20empty%20folder", - permissions: %i[readable writeable]), - [ - Storages::StorageFile.new(id: "a1d45ff742d2175c095f0a7173f93fc3fc23664a953ceae6778fe15398818c2d", - name: "Root", - location: "/", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "58bde0c7931c8f95bb1bf525471146090630cb72827cb1e63dcaab3a9adce763", - name: "Folder with spaces", - location: "/Folder%20with%20spaces") - ] - ) - end - - it_behaves_like "files_query: successful files response" - end - - context "with a path full of umlauts", vcr: "one_drive/files_query_umlauts" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/Folder/Ümlæûts") } - let(:files_result) do - Storages::StorageFiles.new( - [ - Storages::StorageFile.new(id: "01AZJL5PNDURPQGKUSGFCJQJMNNWXKTHSE", - name: "Anrüchiges deutsches Dokument.docx", - size: 18007, - mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - created_at: Time.zone.parse("2023-10-09T15:26:45Z"), - last_modified_at: Time.zone.parse("2023-10-09T15:27:25Z"), - created_by_name: "Eric Schubert", - last_modified_by_name: "Eric Schubert", - location: "/Folder/%C3%9Cml%C3%A6%C3%BBts/Anr%C3%BCchiges%20deutsches%20Dokument.docx", - permissions: %i[readable writeable]) - ], - Storages::StorageFile.new(id: "01AZJL5PNQYF5NM3KWYNA3RJHJIB2XMMMB", - name: "Ümlæûts", - location: "/Folder/%C3%9Cml%C3%A6%C3%BBts", - permissions: %i[readable writeable]), - [ - Storages::StorageFile.new(id: "a1d45ff742d2175c095f0a7173f93fc3fc23664a953ceae6778fe15398818c2d", - name: "Root", - location: "/", - permissions: %i[readable writeable]), - Storages::StorageFile.new(id: "74ccd43303847f2655300641a934959cdb11689ce171aa0f00faa92917fbd340", - name: "Folder", - location: "/Folder") - ] - ) - end - - it_behaves_like "files_query: successful files response" - end - - context "with not existent parent folder", vcr: "one_drive/files_query_invalid_parent" do - let(:folder) { Storages::Peripherals::ParentFolder.new("/I/just/made/that/up") } - let(:error_source) { described_class } - - it_behaves_like "files_query: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query_spec.rb deleted file mode 100644 index 2cd74a9ec51..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/open_file_link_query_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::OpenFileLinkQuery, :vcr, :webmock do - using Storages::Peripherals::ServiceResultRefinements - - let(:user) { create(:user) } - let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } - let(:file_id) { "01AZJL5PJTICED3C5YSVAY6NWTBNA2XERU" } - let(:auth_strategy) do - Storages::Peripherals::Registry.resolve("one_drive.authentication.user_bound").call(user:, storage:) - end - - subject { described_class.new(storage) } - - describe "#call" do - it "responds with correct parameters" do - expect(described_class).to respond_to(:call) - - method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq file_id], - %i[key open_location]) - end - - context "with outbound requests successful" do - context "with open location flag not set", vcr: "one_drive/open_file_link_query_success" do - it "returns the url for opening the file on storage" do - call = subject.call(auth_strategy:, file_id:) - expect(call).to be_success - expect(call.result).to eq("https://finn.sharepoint.com/sites/openprojectfilestoragetests/_layouts/15/Doc.aspx?sourcedoc=%7B3D884033-B88B-4195-8F36-D30B41AB9234%7D&file=Document.docx&action=default&mobileredirect=true") - end - end - - context "with open location flag set", vcr: "one_drive/open_file_link_location_query_success" do - it "returns the url for opening the file on storage" do - call = subject.call(auth_strategy:, file_id:, open_location: true) - expect(call).to be_success - expect(call.result).to eq("https://finn.sharepoint.com/sites/openprojectfilestoragetests/VCR/Folder") - end - end - end - - context "with not existent file id", vcr: "one_drive/open_file_link_query_missing_file_id" do - let(:file_id) { "iamnotexistent" } - - it "must return not found" do - result = subject.call(auth_strategy:, file_id:) - expect(result).to be_failure - expect(result.error_source).to be(Storages::Peripherals::StorageInteraction::OneDrive::Internal::DriveItemQuery) - expect(result.result).to eq(:not_found) - end - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/rename_file_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/rename_file_command_spec.rb deleted file mode 100644 index a72fcfe0055..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/rename_file_command_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::RenameFileCommand, :webmock do - let(:storage) { create(:sharepoint_dev_drive_storage) } - let(:auth_strategy) { Storages::Peripherals::Registry.resolve("one_drive.authentication.userless").call } - - it_behaves_like "rename_file_command: basic command setup" - - it_behaves_like "rename_file_command: validating input data" - - context "when renaming a folder", vcr: "one_drive/rename_file_success" do - let(:file_id) { "01AZJL5PMAXGDWAAKMEBALX4Q6GSN5BSBR" } - let(:name) { "I am the senat" } - - it_behaves_like "rename_file_command: successful file renaming" - end - - context "when renaming a file inside a subdirectory", vcr: "one_drive/rename_file_with_location_success" do - let(:file_id) { "01AZJL5PPMSBBO3R2BIZHJFCELSW3RP7GN" } - let(:name) { "I❤️you death star.png" } - - it_behaves_like "rename_file_command: successful file renaming" - end - - context "when trying to rename a not existent file", vcr: "one_drive/rename_file_not_found" do - let(:file_id) { "sith_have_yellow_light_sabers" } - let(:name) { "this_will_not_happen.png" } - let(:error_source) { described_class } - - it_behaves_like "rename_file_command: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/set_permissions_command_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/set_permissions_command_spec.rb deleted file mode 100644 index 568ae99e4da..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/set_permissions_command_spec.rb +++ /dev/null @@ -1,180 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::SetPermissionsCommand, :webmock do - let(:storage) do - create(:sharepoint_dev_drive_storage, - drive_id: "b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2ODRDvn3haLiQIhB5UYNdqMy") - end - - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials - .strategy - .with_cache(false) - end - - let(:test_folder) do - Storages::Peripherals::Registry - .resolve("one_drive.commands.create_folder") - .call(storage:, - auth_strategy:, - folder_name: "Permission Test Folder", - parent_location: Storages::Peripherals::ParentFolder.new("/")) - .result - end - - it_behaves_like "set_permissions_command: basic command setup" - - context "if folder does not exists", vcr: "one_drive/set_permissions_not_found_folder" do - let(:error_source) { described_class } - let(:input_data) { permission_input_data("THIS_IS_NOT_THE_FOLDER_YOURE_LOOKING_FOR", []) } - - it_behaves_like "set_permissions_command: not found" - end - - context "if a write roles is already set" do - def current_remote_permissions - permission_list_from_role("write") - end - - context "and new write permissions should be set", vcr: "one_drive/set_permissions_replace_permissions_write" do - let(:previous_permissions) { [{ user_id: "84acc1d5-61be-470b-9d79-0d1f105c2c5f", permissions: [:write_files] }] } - let(:replacing_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:write_files] }] } - - it_behaves_like "set_permissions_command: replaces already set permissions" - end - - context "and they should get deleted", vcr: "one_drive/set_permissions_delete_permission_write" do - let(:previous_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:write_files] }] } - let(:replacing_permissions) { [] } - - it_behaves_like "set_permissions_command: replaces already set permissions" - end - end - - context "if a read roles is already set", vcr: "one_drive/set_permissions_replace_permissions_read" do - def current_remote_permissions - permission_list_from_role("read") - end - - context "and new read permissions should be set", vcr: "one_drive/set_permissions_replace_permissions_read" do - let(:previous_permissions) { [{ user_id: "84acc1d5-61be-470b-9d79-0d1f105c2c5f", permissions: [:read_files] }] } - let(:replacing_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files] }] } - - it_behaves_like "set_permissions_command: replaces already set permissions" - end - - context "and they should get deleted", vcr: "one_drive/set_permissions_delete_permission_read" do - let(:previous_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files] }] } - let(:replacing_permissions) { [] } - - it_behaves_like "set_permissions_command: replaces already set permissions" - end - end - - context "if no write permission exists", vcr: "one_drive/set_permissions_create_permission_write" do - let(:user_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:write_files] }] } - - def current_remote_permissions - permission_list_from_role("write") - end - - it_behaves_like "set_permissions_command: creates new permissions" - end - - context "if no read permission exists", vcr: "one_drive/set_permissions_create_permission_read" do - let(:user_permissions) { [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files] }] } - - def current_remote_permissions - permission_list_from_role("read") - end - - it_behaves_like "set_permissions_command: creates new permissions" - end - - context "if a timeout occurs" do - it "logs an error", vcr: "one_drive/set_permissions_delete_permission_read" do - stub_request_with_timeout(:post, /invite$/) - allow(Rails.logger).to receive(:error) - - user_permissions = [{ user_id: "d6e00f6d-1ae7-43e6-b0af-15d99a56d4ce", permissions: [:read_files] }] - input_data = permission_input_data(test_folder.id, user_permissions) - described_class.call(storage:, auth_strategy:, input_data:) - - # rubocop:disable Layout/LineLength - expect(Rails.logger) - .to have_received(:error) - .with( - error_code: :error, - message: nil, - data: %r{timed out while waiting on select \(HTTPX::ConnectTimeoutError\)\n$} - ).once - # rubocop:enable Layout/LineLength - end - end - - private - - def permission_input_data(file_id, user_permissions) - Storages::Peripherals::StorageInteraction::Inputs::SetPermissions.build(file_id:, user_permissions:).value! - end - - def clean_up(file_id) - Storages::Peripherals::Registry - .resolve("one_drive.commands.delete_folder") - .call(storage:, auth_strategy:, location: file_id) - end - - def permission_list_from_role(role) - perm = role == "write" ? :write_files : :read_files - - remote_permissions - .select { |item| item[:roles].first == role } - .map { |grant| grant.dig(:grantedToV2, :user, :id) } - .map { |id| { user_id: id, permissions: [perm] } } - end - - def remote_permissions - Storages::Peripherals::StorageInteraction::Authentication[auth_strategy].call(storage:) do |http| - http.get(Storages::UrlBuilder.url(storage.uri, - "/v1.0/drives", - storage.drive_id, - "/items", - test_folder.id, - "/permissions")) - .raise_for_status - .json(symbolize_keys: true) - .fetch(:value) - end - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/upload_link_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/upload_link_query_spec.rb deleted file mode 100644 index d1d0bfb597e..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/upload_link_query_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::UploadLinkQuery, :webmock do - let(:storage) { create(:sharepoint_dev_drive_storage) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::OAuthClientCredentials.strategy - end - - it_behaves_like "upload_link_query: basic query setup" - - context "when requesting an upload link for an existing file", vcr: "one_drive/upload_link_success" do - let(:upload_data) do - Storages::Peripherals::StorageInteraction::Inputs::UploadData - .build(folder_id: "01AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", file_name: "DeathStart_blueprints.tiff").value! - end - let(:token) do - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBfZGlzcGxheW5hbWUiOiJPcGVuUHJvamVjdCBEZXYgQXBwIiwiYXVkIjoiMDAwMDA" \ - "wMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL2Zpbm4uc2hhcmVwb2ludC5jb21ANGQ0NGJmMzYtOWI1Ni00NWMwLTg4MDctYmJmMzg" \ - "2ZGQwNDdmIiwiY2lkIjoiR3k0SDY0aTF2MEN6NXVxU0tDTkNodz09IiwiZW5kcG9pbnR1cmwiOiJ6cFdrZGttVmxSUEZYRG55eWVmb0thaUg" \ - "ycFhmV0RUdmkvNTVReHVYSlAwPSIsImVuZHBvaW50dXJsTGVuZ3RoIjoiMjc3IiwiZXhwIjoiMTcxNTA5MjYxOCIsImlwYWRkciI6IjIwLjE" \ - "5MC4xOTAuMTAwIiwiaXNsb29wYmFjayI6IlRydWUiLCJpc3MiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAiLCJuYW1" \ - "laWQiOiI0MjYyZGYyYi03N2JiLTQ5YzItYTVkZi0yODM1NWRhNjc2ZDJANGQ0NGJmMzYtOWI1Ni00NWMwLTg4MDctYmJmMzg2ZGQwNDdmIiw" \ - "ibmJmIjoiMTcxNTAwNjIxOCIsInJvbGVzIjoiYWxsc2l0ZXMucmVhZCBhbGxzaXRlcy53cml0ZSBhbGxmaWxlcy53cml0ZSIsInNpdGVpZCI" \ - "6Ik1XSTBZalkxTnpZdE9UQTJaQzAwWkRrMExUaG1ORGt0Tm1Rd01HRTVOVEEzWWpVdyIsInR0IjoiMSIsInZlciI6Imhhc2hlZHByb29mdG9" \ - "rZW4ifQ.UMqPAjuiXSt1rQgFiE0h-k3wkBZ3DmF3I3Nj_zYuYuI" - end - let(:upload_url) do - "https://finn.sharepoint.com/sites/openprojectfilestoragetests/_api/v2.0/drives/" \ - "b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01AZJL5PKRK4XUJQQH3JHIUGK2ALGEJEK4/" \ - "uploadSession?guid=%2789f10eb4-b8d9-4ba9-ab64-eb1e6d39b2ee%27&overwrite=False&rename=True&dc=0" \ - "&tempauth=#{token}" - end - let(:upload_method) { :put } - - it_behaves_like "upload_link_query: successful upload link response" - end - - context "when requesting an upload link for a not existing file", vcr: "one_drive/upload_link_not_found" do - let(:upload_data) do - Storages::Peripherals::StorageInteraction::Inputs::UploadData - .build(folder_id: "04AZJL5PN6Y2GOVW7725BZO354PWSELRRZ", file_name: "DeathStart_blueprints.tiff").value! - end - let(:error_source) { described_class } - - it_behaves_like "upload_link_query: not found" - end -end diff --git a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/user_query_spec.rb b/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/user_query_spec.rb deleted file mode 100644 index d2041f8f517..00000000000 --- a/modules/storages/spec/common/storages/peripherals/storage_interaction/one_drive/user_query_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Peripherals::StorageInteraction::OneDrive::UserQuery, :webmock do - let(:storage) do - create(:sharepoint_dev_drive_storage, - oauth_client_token_user: user) - end - let(:user) { create(:user) } - let(:user_bound_strategy) do - Storages::Peripherals::Registry.resolve("one_drive.authentication.user_bound").call(user:, storage:) - end - let(:auth_strategy) { user_bound_strategy } - - it "is registered" do - expect(Storages::Peripherals::Registry.resolve("one_drive.queries.user")).to eq(described_class) - end - - it "responds to .call" do - expect(described_class).to respond_to(:call) - end - - it ".call takes required parameters" do - method = described_class.method(:call) - - expect(method.parameters).to contain_exactly(%i[keyreq auth_strategy], - %i[keyreq storage]) - end - - it "responds with user details if request is successful", vcr: "one_drive/user_query_success" do - command_result = described_class.call(auth_strategy:, storage:) - - expect(command_result).to be_success - expect(command_result.result).to eq(id: "a9023fd0-c421-4695-b83c-bb3ba67708d6") - end - - it "responds with unauthorized if request is unauthorized", vcr: "one_drive/user_query_unauthorized" do - command_result = described_class.call(auth_strategy:, storage:) - - expect(command_result).to be_failure - expect(command_result.result).to eq(:unauthorized) - end -end diff --git a/modules/storages/spec/components/storages/admin/health/check_result_component_spec.rb b/modules/storages/spec/components/storages/admin/health/check_result_component_spec.rb index 93c71579597..d473b8da305 100644 --- a/modules/storages/spec/components/storages/admin/health/check_result_component_spec.rb +++ b/modules/storages/spec/components/storages/admin/health/check_result_component_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Storages::Admin::Health::CheckResultComponent, type: :component d end context "if check result is successful" do - let(:check_result) { Storages::Peripherals::ConnectionValidators::CheckResult.success(:capabilities_request) } + let(:check_result) { Storages::Adapters::ConnectionValidators::CheckResult.success(:capabilities_request) } it "renders the component" do expect(page).to have_text(I18n.t("storages.health.checks.#{group_key}.#{check_result.key}")) @@ -52,7 +52,7 @@ RSpec.describe Storages::Admin::Health::CheckResultComponent, type: :component d end context "if check result is skipped" do - let(:check_result) { Storages::Peripherals::ConnectionValidators::CheckResult.skipped(:capabilities_request) } + let(:check_result) { Storages::Adapters::ConnectionValidators::CheckResult.skipped(:capabilities_request) } it "renders the component" do expect(page).to have_text(I18n.t("storages.health.checks.#{group_key}.#{check_result.key}")) @@ -66,7 +66,7 @@ RSpec.describe Storages::Admin::Health::CheckResultComponent, type: :component d context "if check result is a warning" do let(:group_key) { :ampf_configuration } let(:check_result) do - Storages::Peripherals::ConnectionValidators::CheckResult.warning(:drive_contents, :od_unexpected_content, nil) + Storages::Adapters::ConnectionValidators::CheckResult.warning(:drive_contents, :od_unexpected_content, nil) end it "renders the component" do @@ -80,7 +80,7 @@ RSpec.describe Storages::Admin::Health::CheckResultComponent, type: :component d context "if check result is a failure" do let(:check_result) do - Storages::Peripherals::ConnectionValidators::CheckResult.failure(:capabilities_request, :unknown_error, nil) + Storages::Adapters::ConnectionValidators::CheckResult.failure(:capabilities_request, :unknown_error, nil) end it "renders the component" do diff --git a/modules/storages/spec/components/storages/admin/health/health_report_component_spec.rb b/modules/storages/spec/components/storages/admin/health/health_report_component_spec.rb index eb5d903bed0..e66a8e4b099 100644 --- a/modules/storages/spec/components/storages/admin/health/health_report_component_spec.rb +++ b/modules/storages/spec/components/storages/admin/health/health_report_component_spec.rb @@ -87,19 +87,19 @@ RSpec.describe Storages::Admin::Health::HealthReportComponent, type: :component private def generate_test_group(group_key, checks) - group = Storages::Peripherals::ConnectionValidators::ValidationGroupResult.new(group_key) + group = Storages::Adapters::ConnectionValidators::ValidationGroupResult.new(group_key) checks.each_with_index do |check, idx| key = :"check_#{idx + 1}" result = case check when :success - Storages::Peripherals::ConnectionValidators::CheckResult.success(key) + Storages::Adapters::ConnectionValidators::CheckResult.success(key) when :warning - Storages::Peripherals::ConnectionValidators::CheckResult.warning(key, :"#{key}_warning", nil) + Storages::Adapters::ConnectionValidators::CheckResult.warning(key, :"#{key}_warning", nil) when :failure - Storages::Peripherals::ConnectionValidators::CheckResult.failure(key, :"#{key}_failure", nil) + Storages::Adapters::ConnectionValidators::CheckResult.failure(key, :"#{key}_failure", nil) else - Storages::Peripherals::ConnectionValidators::CheckResult.skipped(key) + Storages::Adapters::ConnectionValidators::CheckResult.skipped(key) end group.register_check(key) @@ -116,7 +116,7 @@ RSpec.describe Storages::Admin::Health::HealthReportComponent, type: :component def generate_test_report(map) allow(I18n).to receive(:t).and_call_original - report = Storages::Peripherals::ConnectionValidators::ValidatorResult.new + report = Storages::Adapters::ConnectionValidators::ValidatorResult.new map.each_pair do |key, values| result = generate_test_group(key, values) diff --git a/modules/storages/spec/contracts/storages/storages/nextcloud_contract_spec.rb b/modules/storages/spec/contracts/storages/storages/nextcloud_contract_spec.rb deleted file mode 100644 index c27283dcfe6..00000000000 --- a/modules/storages/spec/contracts/storages/storages/nextcloud_contract_spec.rb +++ /dev/null @@ -1,226 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Storages::NextcloudContract, :storage_server_helpers, :webmock do - let(:current_user) { create(:admin) } - let(:storage) { build(:nextcloud_storage) } - let(:mocked_host) { storage.host } - - let!(:capabilities_request) { mock_server_capabilities_response(mocked_host) } - let!(:host_request) { mock_server_config_check_response(mocked_host) } - - # As the NextcloudContract is selected by the BaseContract to make writable attributes available, - # the BaseContract needs to be instantiated here. - subject { Storages::Storages::BaseContract.new(storage, current_user) } - - it "checks the storage url only when changed" do - subject.validate - expect(capabilities_request).to have_been_made.once - expect(host_request).to have_been_made.once - - WebMock.reset_executed_requests! - storage.save - subject.validate - expect(capabilities_request).not_to have_been_made - expect(host_request).not_to have_been_made - end - - describe "Nextcloud application credentials validation" do - context "with valid credentials" do - let(:storage) { build(:nextcloud_storage, :as_automatically_managed) } - - it "passes validation" do - credentials_request = mock_nextcloud_application_credentials_validation(storage.host) - - expect(subject).to be_valid - expect(credentials_request).to have_been_made.once - end - - context "with invalid credentials" do - let(:storage) { build(:nextcloud_storage, :as_automatically_managed) } - - it "fails validation" do - credentials_request = mock_nextcloud_application_credentials_validation(storage.host, response_code: 401) - - expect(subject).not_to be_valid - expect(subject.errors.to_hash).to eq({ password: ["is not valid."] }) - - expect(credentials_request).to have_been_made.once - end - end - - context "with timeout" do - let(:storage) { build(:nextcloud_storage, :as_automatically_managed) } - - it "fails validation" do - credentials_request = mock_nextcloud_application_credentials_validation(storage.host, timeout: true) - - expect(subject).not_to be_valid - expect(subject.errors.to_hash) - .to eq({ password: ["could not be validated. Please check your storage connection and try again."] }) - - # twice due to HTTPX retry plugin being enabled. - expect(credentials_request).to have_been_made.twice - end - end - - context "with unknown error" do - let(:storage) { build(:nextcloud_storage, :as_automatically_managed) } - - it "fails validation" do - credentials_request = mock_nextcloud_application_credentials_validation(storage.host, response_code: 500) - - expect(subject).not_to be_valid - expect(subject.errors.to_hash) - .to eq({ password: ["could not be validated. Please check your storage connection and try again."] }) - - expect(credentials_request).to have_been_made.once - end - end - - context "when the storage is not automatically managed" do - let(:storage) { build(:nextcloud_storage, :as_not_automatically_managed) } - - it "skips credentials validation" do - credentials_request = mock_nextcloud_application_credentials_validation(storage.host) - - expect(subject).to be_valid - expect(credentials_request).not_to have_been_made - end - end - - context "when the storage host has a subpath" do - let(:storage) { build(:nextcloud_storage, :as_automatically_managed, host: "https://host1.example.com/api") } - - it "passes validation" do - credentials_request = mock_nextcloud_application_credentials_validation(storage.host) - - expect(subject).to be_valid - expect(credentials_request).to have_been_made.once - end - end - end - - context "when the storage host is nil" do - let(:storage) { build(:nextcloud_storage, :as_automatically_managed, host: nil) } - let(:mocked_host) { "https://example.com/unrelated" } - - before do - allow(NextcloudApplicationCredentialsValidator).to receive(:new).and_call_original - end - - it "fails validation" do - expect(subject).not_to be_valid - expect(subject.errors.to_hash).to eq({ host: ["is not a valid URL."] }) - expect(NextcloudApplicationCredentialsValidator).not_to have_received(:new) - end - end - end - - describe "authentication_method validation" do - let(:storage) { build(:nextcloud_storage, :as_not_automatically_managed, authentication_method:) } - let(:authentication_method) { "two_way_oauth2" } - - it { is_expected.to be_valid } - - context "when the authentication method is oauth2_sso" do - let(:authentication_method) { "oauth2_sso" } - - before { storage.storage_audience = "valid_audience" } - - it { is_expected.not_to be_valid } - - context "and there is a valid enterprise token", with_ee: [:nextcloud_sso] do - it { is_expected.to be_valid } - end - - context "and the authentication_method has been oauth2_sso before" do - before do - storage.save! # storage is already persisted with this auth method - end - - it { is_expected.to be_valid } - end - end - - context "when the authentication method is unknown" do - let(:authentication_method) { "magic_unicorns" } - - it { is_expected.not_to be_valid } - end - - context "when the authentication method is missing" do - let(:authentication_method) { nil } - - it { is_expected.not_to be_valid } - end - end - - describe "storage_audience validation" do - let(:storage) do - build(:nextcloud_storage, :as_not_automatically_managed, authentication_method:, storage_audience:) - end - - context "when authentication happens through bidirectional OAuth 2.0" do - let(:authentication_method) { "two_way_oauth2" } - - context "and there is no storage_audience" do - let(:storage_audience) { nil } - - it { is_expected.to be_valid } - end - - context "and there is a storage_audience" do - let(:storage_audience) { "nextcloud" } - - it { is_expected.to be_valid } - end - end - - context "when authentication happens through a common IDP", with_ee: [:nextcloud_sso] do - let(:authentication_method) { "oauth2_sso" } - - context "and there is no storage_audience" do - let(:storage_audience) { nil } - - it { is_expected.not_to be_valid } - end - - context "and there is a storage_audience" do - let(:storage_audience) { "nextcloud" } - - it { is_expected.to be_valid } - end - end - end -end diff --git a/modules/storages/spec/contracts/storages/storages/one_drive_contract_spec.rb b/modules/storages/spec/contracts/storages/storages/one_drive_contract_spec.rb deleted file mode 100644 index 2c5019a48fd..00000000000 --- a/modules/storages/spec/contracts/storages/storages/one_drive_contract_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -# 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" -require_module_spec_helper - -RSpec.describe Storages::Storages::OneDriveContract, :storage_server_helpers, :webmock do - let(:current_user) { create(:admin) } - let(:storage) { build(:one_drive_storage) } - - # As the OneDriveContract is selected by the BaseContract to make writable attributes available, - # the BaseContract needs to be instantiated here. - subject(:contract) { Storages::Storages::BaseContract.new(storage, current_user) } - - describe "when a host is set" do - before do - storage.host = "https://exmaple.com/" - end - - it "must be invalid" do - expect(contract).not_to be_valid - end - end - - context "with tenant that is no UUID" do - let(:storage) { build(:one_drive_storage, tenant_id: "123") } - - it "is invalid" do - expect(contract).not_to be_valid - - expect(contract.errors[:tenant_id]).to eq(["is invalid."]) - end - end - - context "with blank Drive ID" do - let(:storage) { build(:one_drive_storage, drive_id: "") } - - it "is invalid" do - expect(contract).not_to be_valid - - expect(contract.errors[:drive_id]).to eq(["can't be blank.", "is too short (minimum is 17 characters)."]) - end - end - - context "with short Drive ID" do - let(:storage) { build(:one_drive_storage, drive_id: "1234567890") } - - it "is invalid" do - expect(contract).not_to be_valid - - expect(contract.errors[:drive_id]).to eq(["is too short (minimum is 17 characters)."]) - end - end -end diff --git a/modules/storages/spec/contracts/storages/storages/one_drive_create_contract_spec.rb b/modules/storages/spec/contracts/storages/storages/one_drive_create_contract_spec.rb index a062a6021ff..60e917b964b 100644 --- a/modules/storages/spec/contracts/storages/storages/one_drive_create_contract_spec.rb +++ b/modules/storages/spec/contracts/storages/storages/one_drive_create_contract_spec.rb @@ -34,11 +34,9 @@ require_relative "shared_contract_examples" RSpec.describe Storages::Storages::CreateContract, with_ee: %i[one_drive_sharepoint_file_storage] do let(:storage) do - build_stubbed(:one_drive_storage, - creator: storage_creator, - name: storage_name, - provider_type: storage_provider_type) + build_stubbed(:one_drive_storage, creator: storage_creator, name: storage_name, provider_type: storage_provider_type) end + let(:contract) { described_class.new(storage, current_user) } it_behaves_like "onedrive storage contract" do diff --git a/modules/storages/spec/controllers/storages/admin/health_status_controller_spec.rb b/modules/storages/spec/controllers/storages/admin/health_status_controller_spec.rb index 2bf4dabb058..ca70c167a07 100644 --- a/modules/storages/spec/controllers/storages/admin/health_status_controller_spec.rb +++ b/modules/storages/spec/controllers/storages/admin/health_status_controller_spec.rb @@ -69,7 +69,7 @@ RSpec.describe Storages::Admin::HealthStatusController do it "sends the text version of the report when requested" do # Creating an actual report result and caching it so we can test the rendering of the response - validator = Storages::Peripherals::Registry["nextcloud.validators.connection"].new(storage) + validator = Storages::Adapters::Registry["nextcloud.validators.connection"].new(storage) result = validator.call Rails.cache.write validator.report_cache_key, result @@ -90,16 +90,16 @@ RSpec.describe Storages::Admin::HealthStatusController do let(:cache_key) { "my_cache_key" } before do - validator = instance_double(Storages::Peripherals::ConnectionValidators::NextcloudValidator) - report = Storages::Peripherals::ConnectionValidators::ValidatorResult.new - allow(Storages::Peripherals::ConnectionValidators::NextcloudValidator).to receive(:new).and_return(validator) + validator = instance_double(Storages::Adapters::Providers::Nextcloud::Validators::ConnectionValidator) + report = Storages::Adapters::ConnectionValidators::ValidatorResult.new + allow(Storages::Adapters::Providers::Nextcloud::Validators::ConnectionValidator).to receive(:new).and_return(validator) allow(validator).to receive_messages(call: report, report_cache_key: cache_key) end it "creates and caches a health status report and redirects to show" do post :create, params: params expect(response.status).to redirect_to admin_settings_storage_health_status_report_path(storage) - expect(Rails.cache.read(cache_key)).to be_a(Storages::Peripherals::ConnectionValidators::ValidatorResult) + expect(Rails.cache.read(cache_key)).to be_a(Storages::Adapters::ConnectionValidators::ValidatorResult) end end @@ -107,9 +107,9 @@ RSpec.describe Storages::Admin::HealthStatusController do let(:cache_key) { "my_cache_key" } before do - validator = instance_double(Storages::Peripherals::ConnectionValidators::NextcloudValidator) - report = Storages::Peripherals::ConnectionValidators::ValidatorResult.new - allow(Storages::Peripherals::ConnectionValidators::NextcloudValidator).to receive(:new).and_return(validator) + validator = instance_double(Storages::Adapters::Providers::Nextcloud::Validators::ConnectionValidator) + report = Storages::Adapters::ConnectionValidators::ValidatorResult.new + allow(Storages::Adapters::Providers::Nextcloud::Validators::ConnectionValidator).to receive(:new).and_return(validator) allow(validator).to receive_messages(call: report, report_cache_key: cache_key) end diff --git a/modules/storages/spec/factories/storage_factory.rb b/modules/storages/spec/factories/storage_factory.rb index 883ee1379c3..3e8e8aae2f6 100644 --- a/modules/storages/spec/factories/storage_factory.rb +++ b/modules/storages/spec/factories/storage_factory.rb @@ -189,14 +189,13 @@ FactoryBot.define do end factory :one_drive_storage_configured, parent: :one_drive_storage do - after(:create) do |storage, _evaluator| + after(:create) do |storage, _| create(:oauth_client, integration: storage) create(:oauth_application, integration: storage) end end - factory :sharepoint_dev_drive_storage, - parent: :one_drive_storage do + factory :sharepoint_dev_drive_storage, parent: :one_drive_storage do automatically_managed { false } transient do diff --git a/modules/storages/spec/factories/storage_file_info_factory.rb b/modules/storages/spec/factories/storage_file_info_factory.rb index 6384ec66c16..5792b6ed7dc 100644 --- a/modules/storages/spec/factories/storage_file_info_factory.rb +++ b/modules/storages/spec/factories/storage_file_info_factory.rb @@ -29,7 +29,7 @@ #++ FactoryBot.define do - factory :storage_file_info, class: "::Storages::StorageFileInfo" do + factory :storage_file_info, class: "::Storages::Adapters::Results::StorageFileInfo" do status { "OK" } status_code { 200 } sequence(:id) { |n| "20000#{n}" } # rubocop:disable FactoryBot/IdSequence diff --git a/modules/storages/spec/features/create_file_links_spec.rb b/modules/storages/spec/features/create_file_links_spec.rb index 27fcb01c9ff..ad1d7fc9bcb 100644 --- a/modules/storages/spec/features/create_file_links_spec.rb +++ b/modules/storages/spec/features/create_file_links_spec.rb @@ -64,15 +64,12 @@ RSpec.describe "Managing file links in work package", :js, :webmock do before do allow(Storages::FileLinkSyncService).to receive(:new).and_return(sync_service) - Storages::Peripherals::Registry.stub( - "#{storage}.queries.user", - ->(_) { ServiceResult.success } - ) + Storages::Adapters::Registry.stub("#{storage}.queries.user", ->(_) { Success() }) stub_request(:propfind, "#{storage.host}remote.php/dav/files/#{remote_identity.origin_user_id}") - .to_return(status: 207, body: root_xml_response, headers: {}) + .to_return(status: 207, body: root_xml_response, headers: { "Content-Type": "application/xml" }) stub_request(:propfind, "#{storage.host}remote.php/dav/files/#{remote_identity.origin_user_id}/Folder1") - .to_return(status: 207, body: folder1_xml_response, headers: {}) + .to_return(status: 207, body: folder1_xml_response, headers: { "Content-Type": "application/xml" }) oauth_client_token project_storage diff --git a/modules/storages/spec/features/show_file_links_spec.rb b/modules/storages/spec/features/show_file_links_spec.rb index fa731acb9ea..81754a4635b 100644 --- a/modules/storages/spec/features/show_file_links_spec.rb +++ b/modules/storages/spec/features/show_file_links_spec.rb @@ -45,18 +45,15 @@ RSpec.describe "Showing of file links in work package", :js do let(:wp_page) { Pages::FullWorkPackage.new(work_package, project) } let(:sync_service) { instance_double(Storages::FileLinkSyncService) } - let(:authorization_state) { ServiceResult.success } + let(:authorization_state) { Success() } let(:remote_identity) { create(:remote_identity, user: current_user, integration: storage) } before do - Storages::Peripherals::Registry.stub( - "#{storage}.queries.user", - ->(_) { authorization_state } - ) + Storages::Adapters::Registry.stub("#{storage}.queries.user", ->(_) { authorization_state }) # Mock FileLinkSyncService as if Nextcloud would respond with origin_status=nil - allow(Storages::FileLinkSyncService) - .to receive(:new).and_return(sync_service) + allow(Storages::FileLinkSyncService).to receive(:new).and_return(sync_service) + allow(sync_service).to receive(:call) do |file_links| ServiceResult.success(result: file_links.each { |file_link| file_link.origin_status = :view_allowed }) end @@ -96,7 +93,7 @@ RSpec.describe "Showing of file links in work package", :js do end context "if user is not connected to Nextcloud" do - let(:remote_identity) { nil } + let(:authorization_state) { Failure(Storages::Adapters::Results::Error.new(code: :missing_token, source: self)) } it "must show storage information box with login button" do within_test_selector("op-tab-content--tab-section", text: "MY STORAGE", wait: 25) do @@ -108,7 +105,7 @@ RSpec.describe "Showing of file links in work package", :js do end context "if user is not authorized in Nextcloud" do - let(:authorization_state) { ServiceResult.failure(errors: Storages::StorageError.new(code: :unauthorized)) } + let(:authorization_state) { Failure(Storages::Adapters::Results::Error.new(code: :unauthorized, source: self)) } it "must show storage information box with login button" do within_test_selector("op-tab-content--tab-section", text: "MY STORAGE", wait: 25) do @@ -120,7 +117,7 @@ RSpec.describe "Showing of file links in work package", :js do end context "if an error occurred while authorizing to Nextcloud" do - let(:authorization_state) { ServiceResult.failure(errors: Storages::StorageError.new(code: :error)) } + let(:authorization_state) { Failure(Storages::Adapters::Results::Error.new(code: :error, source: self)) } it "must show storage information box" do within_test_selector("op-tab-content--tab-section", text: "MY STORAGE", wait: 25) do diff --git a/modules/storages/spec/lib/api/v3/storages/storages_representer_rendering_spec.rb b/modules/storages/spec/lib/api/v3/storages/storages_representer_rendering_spec.rb index 29589853c85..bd0ac8a679e 100644 --- a/modules/storages/spec/lib/api/v3/storages/storages_representer_rendering_spec.rb +++ b/modules/storages/spec/lib/api/v3/storages/storages_representer_rendering_spec.rb @@ -34,16 +34,13 @@ require_module_spec_helper RSpec.describe API::V3::Storages::StorageRepresenter, "rendering" do let(:oauth_client_credentials) { build_stubbed(:oauth_client) } let(:user) { create(:user) } - let(:auth_check_result) { ServiceResult.success } + let(:auth_check_result) { Success() } let(:representer) { described_class.new(storage, current_user: user, embed_links: true) } subject(:generated) { representer.to_json } before do - Storages::Peripherals::Registry.stub( - "#{storage}.queries.user", - ->(_) { auth_check_result } - ) + Storages::Adapters::Registry.stub("#{storage}.queries.user", ->(_) { auth_check_result }) end shared_examples_for "common file storage properties" do @@ -103,7 +100,7 @@ RSpec.describe API::V3::Storages::StorageRepresenter, "rendering" do end context "if authentication check returns unauthorized" do - let(:auth_check_result) { ServiceResult.failure(errors: Storages::StorageError.new(code: :unauthorized)) } + let(:auth_check_result) { Failure(Storages::Adapters::Results::Error.new(code: :unauthorized, source: nil)) } it_behaves_like "has a titled link" do let(:link) { "authorizationState" } @@ -113,7 +110,7 @@ RSpec.describe API::V3::Storages::StorageRepresenter, "rendering" do end context "if authentication check returns error" do - let(:auth_check_result) { ServiceResult.failure(errors: Storages::StorageError.new(code: :error)) } + let(:auth_check_result) { Failure(Storages::Adapters::Results::Error.new(code: :error, source: nil)) } it_behaves_like "has a titled link" do let(:link) { "authorizationState" } @@ -121,16 +118,6 @@ RSpec.describe API::V3::Storages::StorageRepresenter, "rendering" do let(:title) { "Error" } end end - - context "if there is no remote identity for the user at the storage" do - let(:remote_identity) { nil } - - it_behaves_like "has a titled link" do - let(:link) { "authorizationState" } - let(:href) { "urn:openproject-org:api:v3:storages:authorization:NotConnected" } - let(:title) { "Not connected" } - end - end end describe "prepareUpload" do diff --git a/modules/storages/spec/models/storages/project_storage_spec.rb b/modules/storages/spec/models/storages/project_storage_spec.rb index 48b95590c1a..2337a82b349 100644 --- a/modules/storages/spec/models/storages/project_storage_spec.rb +++ b/modules/storages/spec/models/storages/project_storage_spec.rb @@ -31,190 +31,151 @@ require "spec_helper" require_module_spec_helper -RSpec.describe Storages::ProjectStorage do - let(:creator) { create(:user) } - let(:project) { create(:project, enabled_module_names: %i[storages work_packages]) } - let(:storage) { create(:nextcloud_storage) } - let(:attributes) do - { - storage:, - creator:, - project:, - project_folder_mode: :inactive - } - end - - describe "#create" do - it "creates an instance" do - project_storage = described_class.create attributes - expect(project_storage).to be_valid +module Storages + RSpec.describe ProjectStorage do + let(:creator) { create(:user) } + let(:project) { create(:project, enabled_module_names: %i[storages work_packages]) } + let(:storage) { create(:nextcloud_storage) } + let(:attributes) do + { storage:, creator:, project:, project_folder_mode: :inactive } end - context "when having already one instance" do - let(:old_project_storage) { described_class.create attributes } + describe "#create" do + it "creates an instance" do + project_storage = described_class.create attributes + expect(project_storage).to be_valid + end + + context "when having already one instance" do + let(:old_project_storage) { described_class.create attributes } + + before do + old_project_storage + end + + it "fails if it is not unique per storage and project" do + expect(described_class.create(attributes.merge)).not_to be_valid + end + end + end + + describe "#destroy" do + let(:project_storage_to_destroy) { described_class.create(attributes) } + let(:work_package) { create(:work_package, project:) } + let(:file_link) { create(:file_link, storage:, container_id: work_package.id) } before do - old_project_storage + project_storage_to_destroy + file_link + + project_storage_to_destroy.destroy end - it "fails if it is not unique per storage and project" do - expect(described_class.create(attributes.merge)).not_to be_valid + it "does not destroy associated FileLink records" do + expect(described_class.count).to eq 0 + expect(FileLink.count).not_to eq 0 end end - end - describe "#destroy" do - let(:project_storage_to_destroy) { described_class.create(attributes) } - let(:work_package) { create(:work_package, project:) } - let(:file_link) { create(:file_link, storage:, container_id: work_package.id) } + describe "#project_folder_mode_possible?" do + let(:project_storage) { build_stubbed(:project_storage, storage:) } - before do - project_storage_to_destroy - file_link + context "when the storage is automatically managed" do + context "when the storage is a one drive storage" do + let(:storage) { build_stubbed(:one_drive_storage, :as_automatically_managed) } - project_storage_to_destroy.destroy - end + it "returns true for project_folder_mode inactive" do + expect(project_storage.project_folder_mode_possible?("inactive")).to be true + end - it "does not destroy associated FileLink records" do - expect(described_class.count).to eq 0 - expect(Storages::FileLink.count).not_to eq 0 - end - end + it "returns true for project_folder_mode automatic" do + expect(project_storage.project_folder_mode_possible?("automatic")).to be true + end - describe "#project_folder_mode_possible?" do - let(:project_storage) { build_stubbed(:project_storage, storage:) } - - context "when the storage is automatically managed" do - context "when the storage is a one drive storage" do - let(:storage) { build_stubbed(:one_drive_storage, :as_automatically_managed) } - - it "returns true for project_folder_mode inactive" do - expect(project_storage.project_folder_mode_possible?("inactive")).to be true + it "returns false for project_folder_mode manual" do + expect(project_storage.project_folder_mode_possible?("manual")).to be false + end end - it "returns true for project_folder_mode automatic" do - expect(project_storage.project_folder_mode_possible?("automatic")).to be true - end + context "when the storage is a nextcloud storage" do + let(:storage) { build_stubbed(:nextcloud_storage, :as_automatically_managed) } - it "returns false for project_folder_mode manual" do - expect(project_storage.project_folder_mode_possible?("manual")).to be false + it "returns true for project_folder_mode inactive" do + expect(project_storage.project_folder_mode_possible?("inactive")).to be true + end + + it "returns true for project_folder_mode automatic" do + expect(project_storage.project_folder_mode_possible?("automatic")).to be true + end + + it "returns true for project_folder_mode manual" do + expect(project_storage.project_folder_mode_possible?("manual")).to be true + end end end - context "when the storage is a nextcloud storage" do - let(:storage) { build_stubbed(:nextcloud_storage, :as_automatically_managed) } + context "when the storage is not automatically managed" do + context "when the storage is a one drive storage" do + let(:storage) { build_stubbed(:one_drive_storage, :as_not_automatically_managed) } - it "returns true for project_folder_mode inactive" do - expect(project_storage.project_folder_mode_possible?("inactive")).to be true + it "returns true for project_folder_mode inactive" do + expect(project_storage.project_folder_mode_possible?("inactive")).to be true + end + + it "returns false for project_folder_mode automatic" do + expect(project_storage.project_folder_mode_possible?("automatic")).to be false + end + + it "returns true for project_folder_mode manual" do + expect(project_storage.project_folder_mode_possible?("manual")).to be true + end end - it "returns true for project_folder_mode automatic" do - expect(project_storage.project_folder_mode_possible?("automatic")).to be true - end + context "when the storage is a nextcloud storage" do + let(:storage) { build_stubbed(:nextcloud_storage, :as_not_automatically_managed) } - it "returns true for project_folder_mode manual" do - expect(project_storage.project_folder_mode_possible?("manual")).to be true + it "returns true for project_folder_mode inactive" do + expect(project_storage.project_folder_mode_possible?("inactive")).to be true + end + + it "returns false for project_folder_mode automatic" do + expect(project_storage.project_folder_mode_possible?("automatic")).to be false + end + + it "returns true for project_folder_mode manual" do + expect(project_storage.project_folder_mode_possible?("manual")).to be true + end end end end - context "when the storage is not automatically managed" do - context "when the storage is a one drive storage" do - let(:storage) { build_stubbed(:one_drive_storage, :as_not_automatically_managed) } + describe "#project_folder_mode" do + let(:project_storage) { build(:project_storage) } - it "returns true for project_folder_mode inactive" do - expect(project_storage.project_folder_mode_possible?("inactive")).to be true - end - - it "returns false for project_folder_mode automatic" do - expect(project_storage.project_folder_mode_possible?("automatic")).to be false - end - - it "returns true for project_folder_mode manual" do - expect(project_storage.project_folder_mode_possible?("manual")).to be true - end - end - - context "when the storage is a nextcloud storage" do - let(:storage) { build_stubbed(:nextcloud_storage, :as_not_automatically_managed) } - - it "returns true for project_folder_mode inactive" do - expect(project_storage.project_folder_mode_possible?("inactive")).to be true - end - - it "returns false for project_folder_mode automatic" do - expect(project_storage.project_folder_mode_possible?("automatic")).to be false - end - - it "returns true for project_folder_mode manual" do - expect(project_storage.project_folder_mode_possible?("manual")).to be true - end - end - end - end - - describe "#project_folder_mode" do - let(:project_storage) { build(:project_storage) } - - it do - expect(project_storage).to define_enum_for(:project_folder_mode) - .with_values(inactive: "inactive", manual: "manual", automatic: "automatic") - .with_prefix(:project_folder) - .backed_by_column_of_type(:string) - end - end - - describe "#open" do - let(:user) { create(:user, member_with_permissions: { project => permissions }) } - let(:permissions) { %i[] } - let(:project_storage) do - build(:project_storage, - storage:, - project_folder_mode:, - project_folder_id:, - project:) - end - let(:project_folder_id) { nil } - - context "when inactive" do - let(:project_folder_mode) { "inactive" } - - it "opens storage" do - expect(project_storage.open(user).result).to eq("#{storage.host}index.php/apps/files") + it do + expect(project_storage).to define_enum_for(:project_folder_mode) + .with_values(inactive: "inactive", manual: "manual", automatic: "automatic") + .with_prefix(:project_folder) + .backed_by_column_of_type(:string) end end - context "when manual" do - let(:project_folder_mode) { "manual" } + describe "#open" do + let(:user) { create(:user, member_with_permissions: { project => permissions }) } + let(:permissions) { %i[] } + let(:project_storage) { build(:project_storage, storage:, project_folder_mode:, project_folder_id:, project:) } + let(:project_folder_id) { nil } - context "when project_folder_id is missing" do - it "opens storage" do - expect(project_storage.open(user).result).to eq("#{storage.host}index.php/apps/files") - end - end - - context "when project_folder_id is present" do - let(:project_folder_id) { "123" } - - it "opens project_folder" do - expect(project_storage.open(user).result).to eq("#{storage.host}index.php/f/123?openfile=1") - end - end - end - - context "when automatic" do - let(:project_folder_mode) { "automatic" } - - context "when user has no permissions to read files in storage" do - let(:project_folder_mode) { "automatic" } + context "when inactive" do + let(:project_folder_mode) { "inactive" } it "opens storage" do expect(project_storage.open(user).result).to eq("#{storage.host}index.php/apps/files") end end - context "when user has permissions to read files in storage" do - let(:permissions) { %i[read_files] } + context "when manual" do + let(:project_folder_mode) { "manual" } context "when project_folder_id is missing" do it "opens storage" do @@ -230,6 +191,36 @@ RSpec.describe Storages::ProjectStorage do end end end + + context "when automatic" do + let(:project_folder_mode) { "automatic" } + + context "when user has no permissions to read files in storage" do + let(:project_folder_mode) { "automatic" } + + it "opens storage" do + expect(project_storage.open(user).result).to eq("#{storage.host}index.php/apps/files") + end + end + + context "when user has permissions to read files in storage" do + let(:permissions) { %i[read_files] } + + context "when project_folder_id is missing" do + it "opens storage" do + expect(project_storage.open(user).result).to eq("#{storage.host}index.php/apps/files") + end + end + + context "when project_folder_id is present" do + let(:project_folder_id) { "123" } + + it "opens project_folder" do + expect(project_storage.open(user).result).to eq("#{storage.host}index.php/f/123?openfile=1") + end + end + end + end end end end diff --git a/modules/storages/spec/models/storages/storage_spec.rb b/modules/storages/spec/models/storages/storage_spec.rb index 10e4a32fbd7..6c6bd47d96e 100644 --- a/modules/storages/spec/models/storages/storage_spec.rb +++ b/modules/storages/spec/models/storages/storage_spec.rb @@ -36,16 +36,8 @@ RSpec.describe Storages::Storage do let(:storage) { build(:storage, oauth_client:) } let(:oauth_client) { create(:oauth_client) } let(:user) { create(:user) } - let(:selector_class) { Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector } - let(:mocked_instance) { instance_double(selector_class, authentication_method:) } - - before do - allow(selector_class).to receive(:new).and_return(mocked_instance) - end context "when user is authenticated through storage oauth" do - let(:authentication_method) { :storage_oauth } - it "responds with true if oauth_client_token exists" do create(:oauth_client_token, user: user, oauth_client:) expect(storage.oauth_access_granted?(user)).to be true @@ -57,7 +49,9 @@ RSpec.describe Storages::Storage do end context "when user is authenticated through sso" do - let(:authentication_method) { :sso } + let(:provider) { create(:oidc_provider) } + let(:user) { create(:user, authentication_provider: provider) } + let(:storage) { build(:nextcloud_storage, :oidc_sso_enabled) } it "responds with true" do expect(storage.oauth_access_granted?(user)).to be true diff --git a/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb b/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb index 4821f40f964..878c224a6bf 100644 --- a/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb +++ b/modules/storages/spec/requests/api/v3/file_links/file_links_api_spec.rb @@ -606,9 +606,9 @@ RSpec.describe "API v3 file links resource" do describe "with successful response" do before do - Storages::Peripherals::Registry.stub( + Storages::Adapters::Registry.stub( "nextcloud.queries.download_link", - ->(_) { ServiceResult.success(result: url) } + ->(_) { Success(url) } ) end @@ -622,9 +622,9 @@ RSpec.describe "API v3 file links resource" do describe "with query failed" do before do - Storages::Peripherals::Registry.stub( + Storages::Adapters::Registry.stub( "nextcloud.queries.download_link", - ->(_) { ServiceResult.failure(result: error, errors: Storages::StorageError.new(code: error)) } + ->(_) { Failure(Storages::Adapters::Results::Error.new(source: self, code: error)) } ) get path diff --git a/modules/storages/spec/requests/api/v3/project_storages/project_storages_spec.rb b/modules/storages/spec/requests/api/v3/project_storages/project_storages_spec.rb index d59495bb8ff..2d9b5754fb6 100644 --- a/modules/storages/spec/requests/api/v3/project_storages/project_storages_spec.rb +++ b/modules/storages/spec/requests/api/v3/project_storages/project_storages_spec.rb @@ -48,9 +48,7 @@ RSpec.describe "API v3 project storages resource", :webmock, content_type: :json shared_let(:project_storage23) { create(:project_storage, project: project2, storage: storage3) } shared_let(:project_storage21) { create(:project_storage, project: project2, storage: storage1) } shared_let(:project_storage31) { create(:project_storage, project: project3, storage: storage1) } - subject(:last_response) do - get path - end + subject(:last_response) { get path } before { login_as current_user } @@ -254,14 +252,8 @@ RSpec.describe "API v3 project storages resource", :webmock, content_type: :json end before do - Storages::Peripherals::Registry.stub( - "nextcloud.queries.open_storage", - ->(_) { ServiceResult.success(result: location) } - ) - Storages::Peripherals::Registry.stub( - "nextcloud.queries.open_file_link", - ->(_) { ServiceResult.success(result: location_project_folder) } - ) + Storages::Adapters::Registry.stub("nextcloud.queries.open_storage", ->(_) { Success(location) }) + Storages::Adapters::Registry.stub("nextcloud.queries.open_file_link", ->(_) { Success(location_project_folder) }) end context "as admin" do @@ -274,19 +266,8 @@ RSpec.describe "API v3 project storages resource", :webmock, content_type: :json it_behaves_like "redirect response" context "if project storage has a configured project folder" do - before(:all) do - project_storage12.update( - project_folder_id: "1337", - project_folder_mode: "manual" - ) - end - - after(:all) do - project_storage12.update( - project_folder_id: nil, - project_folder_mode: "inactive" - ) - end + before { project_storage12.update(project_folder_id: "1337", project_folder_mode: "manual") } + after { project_storage12.update(project_folder_id: nil, project_folder_mode: "inactive") } let(:path) { api_v3_paths.project_storage_open(project_storage12.id) } diff --git a/modules/storages/spec/requests/api/v3/storages/storage_files_api_spec.rb b/modules/storages/spec/requests/api/v3/storages/storage_files_api_spec.rb index 096b26b63fe..0769c3b87f9 100644 --- a/modules/storages/spec/requests/api/v3/storages/storage_files_api_spec.rb +++ b/modules/storages/spec/requests/api/v3/storages/storage_files_api_spec.rb @@ -37,19 +37,17 @@ RSpec.describe "API v3 storage files", :storage_server_helpers, :webmock, conten let(:permissions) { %i(view_work_packages view_file_links) } let(:project) { create(:project) } - let(:current_user) do - create(:user, member_with_permissions: { project => permissions }) + let(:current_user) { create(:user, member_with_permissions: { project => permissions }) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_not_automatically_managed, + oauth_client_token_user: current_user, origin_user_id: "m.jade@death.star") end - let(:oauth_application) { create(:oauth_application) } - let(:storage) { create(:nextcloud_storage_configured, creator: current_user, oauth_application:) } - let(:oauth_token) { create(:oauth_client_token, user: current_user, oauth_client: storage.oauth_client) } let(:project_storage) { create(:project_storage, project:, storage:) } subject(:last_response) { get path } before do - oauth_application project_storage login_as current_user end @@ -58,60 +56,57 @@ RSpec.describe "API v3 storage files", :storage_server_helpers, :webmock, conten let(:path) { api_v3_paths.storage_files(storage.id) } let(:response) do - Storages::StorageFiles.new( + Storages::Adapters::Results::StorageFileCollection.new( [ - Storages::StorageFile.new( - id: 1, - name: "new_younglings.md", - size: 4096, - mime_type: "text/markdown", - created_at: DateTime.now, - last_modified_at: DateTime.now, - created_by_name: "Obi-Wan Kenobi", - last_modified_by_name: "Obi-Wan Kenobi", - location: "/", - permissions: %i[readable] - ), - Storages::StorageFile.new( - id: 2, - name: "holocron_inventory.md", - size: 4096, - mime_type: "text/markdown", - created_at: DateTime.now, - last_modified_at: DateTime.now, - created_by_name: "Obi-Wan Kenobi", - last_modified_by_name: "Obi-Wan Kenobi", - location: "/", - permissions: %i[readable writeable] - ) + Storages::Adapters::Results::StorageFile.new(id: "555", + name: "Folder", + size: 232167, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder", + permissions: %i[readable writeable]), + Storages::Adapters::Results::StorageFile.new(id: "561", + name: "Folder with spaces", + size: 890, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:52:09Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/Folder%20with%20spaces", + permissions: %i[readable writeable]), + Storages::Adapters::Results::StorageFile.new(id: "562", + name: "Ümlæûts", + size: 19720, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:51:48Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/%c3%9cml%c3%a6%c3%bbts", + permissions: %i[readable writeable]) ], - Storages::StorageFile.new( - id: 32, - name: "/", - size: 4096 * 2, - mime_type: "application/x-op-directory", - created_at: DateTime.now, - last_modified_at: DateTime.now, - created_by_name: "Obi-Wan Kenobi", - last_modified_by_name: "Obi-Wan Kenobi", - location: "/", - permissions: %i[readable writeable] - ), + Storages::Adapters::Results::StorageFile.new(id: "385", + name: "Root", + size: 252777, + mime_type: "application/x-op-directory", + created_at: nil, + last_modified_at: Time.zone.parse("2024-08-09T11:53:42Z"), + created_by_name: "Mara Jade", + last_modified_by_name: nil, + location: "/", + permissions: %i[readable writeable]), [] ) end context "with successful response" do - before do - Storages::Peripherals::Registry.stub( - "nextcloud.queries.files", - ->(_) { ServiceResult.success(result: response) } - ) - end - subject { last_response.body } - it "responds with appropriate JSON" do + it "responds with appropriate JSON", vcr: "nextcloud/files_query_root" do expect(subject).to be_json_eql(response.files[0].id.to_json).at_path("files/0/id") expect(subject).to be_json_eql(response.files[0].name.to_json).at_path("files/0/name") expect(subject).to be_json_eql(response.files[1].id.to_json).at_path("files/1/id") @@ -126,16 +121,16 @@ RSpec.describe "API v3 storage files", :storage_server_helpers, :webmock, conten context "with query failed" do before do - Storages::Peripherals::Registry.stub( + Storages::Adapters::Registry.stub( "nextcloud.queries.files", - ->(_) { ServiceResult.failure(result: error, errors: Storages::StorageError.new(code: error)) } + ->(_) { Failure(Storages::Adapters::Results::Error.new(source: self, code: error)) } ) end context "with authorization failure" do let(:error) { :unauthorized } - it { expect(last_response).to have_http_status(:internal_server_error) } + it { expect(last_response).to have_http_status(:unauthorized) } end context "with internal error" do @@ -159,7 +154,7 @@ RSpec.describe "API v3 storage files", :storage_server_helpers, :webmock, conten end describe "GET /api/v3/storages/:storage_id/files/:file_id" do - let(:file_id) { "42" } + let(:file_id) { "350" } let(:path) { api_v3_paths.storage_file(storage.id, file_id) } context "with successful response" do @@ -168,35 +163,30 @@ RSpec.describe "API v3 storage files", :storage_server_helpers, :webmock, conten status: "OK", status_code: 200, id: file_id, - name: "Documents", + name: "Ümlæûts", last_modified_at: DateTime.now, created_at: DateTime.now, mime_type: "application/x-op-directory", - size: 1108864, - owner_name: "Darth Vader", - owner_id: "darthvader", + size: 19720, + owner_name: "admin", + owner_id: "admin", last_modified_by_name: "Darth Sidious", last_modified_by_id: "palpatine", permissions: "RGDNVCK", - location: "/Documents" + location: "/Folder/%C3%9Cml%C3%A6%C3%BBts" ) end - before do - files_info_query = ->(_) { ServiceResult.success(result: response) } - Storages::Peripherals::Registry.stub("nextcloud.queries.file_info", files_info_query) - end - subject { last_response.body } - it "responds with appropriate JSON" do + it "responds with appropriate JSON", vcr: "nextcloud/file_info_query_success_folder" do expect(subject).to be_json_eql("StorageFile".to_json).at_path("_type") expect(subject).to be_json_eql(response.id.to_json).at_path("id") expect(subject).to be_json_eql(response.name.to_json).at_path("name") expect(subject).to be_json_eql(response.size.to_json).at_path("size") expect(subject).to be_json_eql(response.mime_type.to_json).at_path("mimeType") + expect(subject).to be_json_eql(response.owner_name.to_json).at_path("createdByName") - expect(subject).to be_json_eql(response.last_modified_by_name.to_json).at_path("lastModifiedByName") expect(subject).to be_json_eql(response.location.to_json).at_path("location") expect(subject).to be_json_eql(response.permissions.to_json).at_path("permissions") end @@ -204,9 +194,9 @@ RSpec.describe "API v3 storage files", :storage_server_helpers, :webmock, conten context "with query failed" do before do - Storages::Peripherals::Registry + Storages::Adapters::Registry .stub("#{storage}.queries.file_info", - ->(_) { ServiceResult.failure(result: error, errors: Storages::StorageError.new(code: error)) }) + ->(_) { Failure(Storages::Adapters::Results::Error.new(code: error, source: self)) }) end context "with authorization failure" do @@ -244,37 +234,33 @@ RSpec.describe "API v3 storage files", :storage_server_helpers, :webmock, conten describe "POST /api/v3/storages/:storage_id/files/prepare_upload" do let(:permissions) { %i(view_work_packages view_file_links manage_file_links) } let(:path) { api_v3_paths.prepare_upload(storage.id) } - let(:upload_link) { Storages::UploadLink.new("https://example.com/upload/xyz123", :post) } + + let(:destination) { %r|direct-upload/SrQJeC5zM3B5Gw64d7dEQFQpFw8YBAtZWoxeLb59AR7PpGPyoGAkAko5G6ZiZ2HA| } let(:body) { { fileName: "ape.png", parent: "/Pictures", projectId: project.id }.to_json } - subject(:last_response) do - post(path, body) - end + let(:last_response) { post(path, body) } describe "with successful response" do - before do - Storages::Peripherals::Registry - .stub("nextcloud.queries.upload_link", ->(_) { ServiceResult.success(result: upload_link) }) - end - subject { last_response.body } - it "responds with appropriate JSON" do - expect(subject).to be_json_eql(Storages::UploadLink.name.split("::").last.to_json).at_path("_type") + it "responds with appropriate JSON", vcr: "nextcloud/upload_link_success" do + expect(subject).to be_json_eql("UploadLink".to_json).at_path("_type") expect(subject) .to(be_json_eql("#{API::V3::URN_PREFIX}storages:upload_link:no_link_provided".to_json) .at_path("_links/self/href")) - expect(subject).to be_json_eql(upload_link.destination.to_json).at_path("_links/destination/href") expect(subject).to be_json_eql("post".to_json).at_path("_links/destination/method") expect(subject).to be_json_eql("Upload File".to_json).at_path("_links/destination/title") + + href = MultiJson.load(subject).dig("_links", "destination", "href") + expect(href).to match(destination) end end context "with query failed" do before do - Storages::Peripherals::Registry.stub( + Storages::Adapters::Registry.stub( "nextcloud.queries.upload_link", - ->(_) { ServiceResult.failure(result: error, errors: Storages::StorageError.new(code: error)) } + ->(_) { Failure(Storages::Adapters::Results::Error.new(code: error, source: self)) } ) end diff --git a/modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb b/modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb index beeae1561a0..ad7cf60ad72 100644 --- a/modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb +++ b/modules/storages/spec/requests/api/v3/storages/storage_folders_api_spec.rb @@ -37,22 +37,19 @@ RSpec.describe "API v3 storage folders", :storage_server_helpers, :webmock, cont let(:permissions) { %i(view_work_packages view_file_links manage_file_links) } let(:project) { create(:project) } - let(:current_user) do - create(:user, member_with_permissions: { project => permissions }) - end + let(:current_user) { create(:user, member_with_permissions: { project => permissions }) } - let(:oauth_application) { create(:oauth_application) } - let(:storage) { create(:nextcloud_storage_configured, creator: current_user, oauth_application:) } + let(:storage) { create(:nextcloud_storage_configured, creator: current_user) } let(:oauth_token) { create(:oauth_client_token, user: current_user, oauth_client: storage.oauth_client) } - let(:project_storage) { create(:project_storage, project:, storage:) } + let!(:project_storage) { create(:project_storage, project:, storage:) } + + let(:create_folder_double) { class_double(Storages::Adapters::Providers::Nextcloud::Commands::CreateFolderCommand) } + let(:auth_strategy) { Storages::Adapters::Registry["nextcloud.authentication.user_bound"].call(current_user, storage) } + let(:input_data) { Storages::Adapters::Input::CreateFolder.build(folder_name:, parent_location: "/").value! } subject(:last_response) { post(path, body) } - before do - oauth_application - project_storage - login_as current_user - end + before { login_as current_user } describe "POST /api/v3/storages/:storage_id/folders" do let(:path) { api_v3_paths.storage_folders(storage.id) } @@ -60,52 +57,47 @@ RSpec.describe "API v3 storage folders", :storage_server_helpers, :webmock, cont let(:folder_name) { "TestFolder" } let(:response) do - Storages::StorageFile.new( - id: 1, + Storages::Adapters::Results::StorageFile.build( + id: "1", name: folder_name, size: 128, mime_type: "application/x-op-directory", - created_at: DateTime.now, - last_modified_at: DateTime.now, + created_at: Time.zone.now, + last_modified_at: Time.zone.now, created_by_name: "Obi-Wan Kenobi", last_modified_by_name: "Obi-Wan Kenobi", location: "/", permissions: %i[readable] - ) + ).value! end let(:file_info) do - Storages::StorageFileInfo.new( + Storages::Adapters::Results::StorageFileInfo.build( status: "OK", status_code: 200, id: SecureRandom.hex, name: "/", location: "/" - ) + ).value! end before do - file_info_mock = class_double(Storages::Peripherals::StorageInteraction::Nextcloud::FileInfoQuery) + file_info_mock = class_double(Storages::Adapters::Providers::Nextcloud::Queries::FileInfoQuery) allow(file_info_mock).to receive(:call).with( - storage: storage, - auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - file_id: file_info.id - ).and_return(ServiceResult.success(result: file_info)) - Storages::Peripherals::Registry.stub("nextcloud.queries.file_info", file_info_mock) + storage:, + auth_strategy:, + input_data: Storages::Adapters::Input::FileInfo.build(file_id: file_info.id).value! + ).and_return(Success(file_info)) + Storages::Adapters::Registry.stub("nextcloud.queries.file_info", file_info_mock) end context "with successful response" do subject { last_response.body } before do - create_folder_mock = class_double(Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) - allow(create_folder_mock).to receive(:call).with( - storage: storage, - auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - folder_name:, - parent_location: instance_of(Storages::Peripherals::ParentFolder) - ).and_return(ServiceResult.success(result: response)) - Storages::Peripherals::Registry.stub("nextcloud.commands.create_folder", create_folder_mock) + allow(create_folder_double).to receive(:call).with(storage:, auth_strategy:, input_data:).and_return(Success(response)) + + Storages::Adapters::Registry.stub("nextcloud.commands.create_folder", create_folder_double) end it "responds with appropriate JSON" do @@ -116,21 +108,18 @@ RSpec.describe "API v3 storage folders", :storage_server_helpers, :webmock, cont end context "with query failed" do + let(:error_result) { Storages::Adapters::Results::Error.new(code: error, source: self) } + before do - create_folder_mock = class_double(Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) - allow(create_folder_mock).to receive(:call).with( - storage: storage, - auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - folder_name:, - parent_location: instance_of(Storages::Peripherals::ParentFolder) - ).and_return(ServiceResult.failure(result: error, errors: Storages::StorageError.new(code: error))) - Storages::Peripherals::Registry.stub("nextcloud.commands.create_folder", create_folder_mock) + allow(create_folder_double).to receive(:call).with(storage:, auth_strategy:, input_data:) + .and_return(Failure(error_result)) + Storages::Adapters::Registry.stub("nextcloud.commands.create_folder", create_folder_double) end context "with authorization failure" do let(:error) { :unauthorized } - it { expect(last_response).to have_http_status(:internal_server_error) } + it { expect(last_response).to have_http_status(:unauthorized) } end context "with internal error" do diff --git a/modules/storages/spec/requests/api/v3/storages/storages_api_spec.rb b/modules/storages/spec/requests/api/v3/storages/storages_api_spec.rb index 901fe3117e2..53ab7bb4c1d 100644 --- a/modules/storages/spec/requests/api/v3/storages/storages_api_spec.rb +++ b/modules/storages/spec/requests/api/v3/storages/storages_api_spec.rb @@ -47,19 +47,17 @@ RSpec.describe "API v3 storages resource", :storage_server_helpers, :webmock, co shared_let(:project_storage) { create(:project_storage, project:, storage:) } let(:current_user) { user_with_permissions } - let(:auth_check_result) { ServiceResult.success } + let(:user_query_result) { Success(id: "greedo") } - subject(:last_response) do - get path - end + subject(:last_response) { get path } before do - Storages::Peripherals::Registry.stub("nextcloud.queries.user", ->(_) { auth_check_result }) + Storages::Adapters::Registry.stub("nextcloud.queries.user", ->(_) { user_query_result }) login_as current_user end shared_examples_for "successful storage response" do |as_admin: false| - include_examples "successful response" + it_behaves_like "successful response" describe "response body" do subject { last_response.body } @@ -233,8 +231,8 @@ RSpec.describe "API v3 storages resource", :storage_server_helpers, :webmock, co it_behaves_like "successful storage response" context "if user is missing permission view_file_links" do - before(:all) { remove_permissions(user_with_permissions, :view_file_links) } - after(:all) { add_permissions(user_with_permissions, :view_file_links) } + before { remove_permissions(user_with_permissions, :view_file_links) } + after { add_permissions(user_with_permissions, :view_file_links) } it_behaves_like "not found" end @@ -280,40 +278,30 @@ RSpec.describe "API v3 storages resource", :storage_server_helpers, :webmock, co end context "when user has a remote identity for the storage" do - before do - create :remote_identity, user: current_user, integration: storage - end + before { create(:remote_identity, user: current_user, integration: storage) } context "when authorization succeeds and storage is connected" do - let(:auth_check_result) { ServiceResult.success } - - include_examples "a storage authorization result", - expected: API::V3::Storages::URN_CONNECTION_CONNECTED, - has_authorize_link: false + it_behaves_like "a storage authorization result", + expected: API::V3::Storages::URN_CONNECTION_CONNECTED, + has_authorize_link: false end context "when authorization fails" do - let(:auth_check_result) { ServiceResult.failure(errors: Storages::StorageError.new(code: :unauthorized)) } + let(:user_query_result) { Failure(Storages::Adapters::Results::Error.new(code: :unauthorized, source: self)) } - include_examples "a storage authorization result", - expected: API::V3::Storages::URN_CONNECTION_AUTH_FAILED, - has_authorize_link: true + it_behaves_like "a storage authorization result", + expected: API::V3::Storages::URN_CONNECTION_AUTH_FAILED, + has_authorize_link: true end context "when authorization fails with an error" do - let(:auth_check_result) { ServiceResult.failure(errors: Storages::StorageError.new(code: :error)) } + let(:user_query_result) { Failure(Storages::Adapters::Results::Error.new(code: :error, source: self)) } - include_examples "a storage authorization result", - expected: API::V3::Storages::URN_CONNECTION_ERROR, - has_authorize_link: false + it_behaves_like "a storage authorization result", + expected: API::V3::Storages::URN_CONNECTION_ERROR, + has_authorize_link: false end end - - context "when user has no remote identity for the storage" do - include_examples "a storage authorization result", - expected: API::V3::Storages::URN_CONNECTION_NOT_CONNECTED, - has_authorize_link: true - end end end @@ -322,9 +310,7 @@ RSpec.describe "API v3 storages resource", :storage_server_helpers, :webmock, co let(:name) { "A new storage name" } let(:params) { { name: } } - subject(:last_response) do - patch path, params.to_json - end + subject(:last_response) { patch path, params.to_json } context "as non-admin" do context "if user belongs to a project using the given storage" do @@ -445,13 +431,10 @@ RSpec.describe "API v3 storages resource", :storage_server_helpers, :webmock, co describe "GET /api/v3/storages/:storage_id/open" do let(:path) { api_v3_paths.storage_open(storage.id) } - let(:location) { "https://deathstar.storage.org/files" } + let(:location) { URI("https://deathstar.storage.org/files") } before do - Storages::Peripherals::Registry.stub( - "nextcloud.queries.open_storage", - ->(_) { ServiceResult.success(result: location) } - ) + Storages::Adapters::Registry.stub("nextcloud.queries.open_storage", ->(_) { Success(location) }) end context "as admin" do @@ -464,8 +447,8 @@ RSpec.describe "API v3 storages resource", :storage_server_helpers, :webmock, co it_behaves_like "redirect response" context "if user is missing permission view_file_links" do - before(:all) { remove_permissions(user_with_permissions, :view_file_links) } - after(:all) { add_permissions(user_with_permissions, :view_file_links) } + before { remove_permissions(user_with_permissions, :view_file_links) } + after { add_permissions(user_with_permissions, :view_file_links) } it_behaves_like "not found" end diff --git a/modules/storages/spec/requests/project_storages_open_spec.rb b/modules/storages/spec/requests/project_storages_open_spec.rb index c5c498b3fc2..77afe040e8e 100644 --- a/modules/storages/spec/requests/project_storages_open_spec.rb +++ b/modules/storages/spec/requests/project_storages_open_spec.rb @@ -31,40 +31,26 @@ require "spec_helper" require_module_spec_helper -RSpec.describe "projects/:project_id/project_storages/:id/open" do +RSpec.describe "projects/:project_id/project_storages/:id/open", :webmock do + let(:storage) { create(:nextcloud_storage_configured, :as_automatically_managed) } + let(:project_storage) { create(:project_storage, storage:, project_folder_id: "123", project_folder_mode:) } + let(:project) { project_storage.project } + + let(:project_folder_mode) { "automatic" } + let(:authorization_state) { :connected } + + let(:path) { "projects/#{project.identifier}/project_storages/#{project_storage.id}/open" } + let(:permissions) { %i[view_file_links read_files] } + let(:user_query_result) { Success(:i_am_authorized) } + + current_user { create(:user, member_with_permissions: { project => permissions }) } + subject(:request) do get path, {}, { "HTTP_ACCEPT" => "text/html" } end - current_user { create(:user, member_with_permissions: { project => permissions }) } - let(:permissions) { %i[view_file_links read_files] } - let(:project_storage) { create(:project_storage, storage:, project_folder_id: "123", project_folder_mode:) } - let(:project) { project_storage.project } - let(:storage) { create(:nextcloud_storage_configured) } - let(:project_folder_mode) { "automatic" } - let(:authorization_state) { :connected } - let(:path) { "projects/#{project.identifier}/project_storages/#{project_storage.id}/open" } - let(:auth_method_selector) do - instance_double( - Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector, - storage_oauth?: authentication_method != :sso, - sso?: authentication_method == :sso, - authentication_method: - ) - end - let(:authentication_method) { :storage_oauth } - let(:folder_create_service) { class_double(Storages::NextcloudManagedFolderCreateService) } - let(:folder_permissions_service) { class_double(Storages::NextcloudManagedFolderPermissionsService) } - let(:file_info_result) { ServiceResult.success } - before do - Storages::Peripherals::Registry.stub("nextcloud.queries.file_info", ->(_) { file_info_result }) - Storages::Peripherals::Registry.stub("nextcloud.services.folder_create", folder_create_service) - Storages::Peripherals::Registry.stub("nextcloud.services.folder_permissions", folder_permissions_service) - allow(Storages::Peripherals::StorageInteraction::Authentication).to receive(:authorization_state) - .and_return(authorization_state) - allow(Storages::Peripherals::StorageInteraction::AuthenticationMethodSelector).to receive(:new) - .and_return(auth_method_selector) + Storages::Adapters::Registry.stub("nextcloud.queries.user", ->(*) { user_query_result }) end it "redirects to the project folder in the storage" do @@ -104,11 +90,9 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do context "when an error occurs in determining the target location" do before do - Storages::Peripherals::Registry.stub("nextcloud.queries.open_file_link", ->(_) do - ServiceResult.failure( - errors: Storages::StorageError.new(code: 400, log_message: "Request made outside opening hours.") - ) - end) + Storages::Adapters::Registry + .stub("nextcloud.queries.open_file_link", + ->(_) { Failure(Storages::Adapters::Results::Error.new(code: :error, source: self)) }) end it "renders an error message", :aggregate_failures do @@ -117,12 +101,12 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do expect(last_response.headers["Location"]).to eq("http://test.host/projects/#{project.id}") flash = Sessions::UserSession.last.data.dig("flash", "flashes") - expect(flash["error"]).to eq("400 | Request made outside opening hours.") + expect(flash["error"]).to eq("error") end end - context "when the user has no remote identity yet" do - let(:authorization_state) { :not_connected } + context "when the user has no current token" do + let(:user_query_result) { Failure(Storages::Adapters::Results::Error.new(code: :missing_token, source: self)) } context "and the user authenticates through OAuth 2.0 at the storage" do it "ensures creation of a remote identity" do @@ -137,20 +121,29 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do end context "and the user authenticates through a common SSO IDP" do - let(:authentication_method) { :sso } + let(:oidc_provider) { create(:oidc_provider, :token_exchange_capable) } + let(:storage) { create(:nextcloud_storage, :oidc_sso_enabled) } + + let(:user_query_result) { Failure(Storages::Adapters::Results::Error.new(code: :unauthorized, source: self)) } + + current_user do + create(:user, authentication_provider: oidc_provider, member_with_permissions: { project => permissions }) + end it "redirects to the project folder in the storage" do request expect(last_response).to have_http_status(:found) - expect(last_response.headers["Location"]).to eq("#{storage.host}index.php/f/123?openfile=1") + expect(last_response.headers["Location"]).to eq("http://test.host/projects/#{project.id}") + flash = Sessions::UserSession.last.data.dig("flash", "flashes") + expect(flash["error"]).to be_present end end end - context "when we can't determine the user's remote identity" do - let(:authorization_state) { :error } - + context "when we can't authenticate the user" do context "and the user authenticates through OAuth 2.0 at the storage" do + let(:user_query_result) { Failure(Storages::Adapters::Results::Error.new(code: :unauthorized, source: self)) } + it "renders an error message", :aggregate_failures do request @@ -162,12 +155,20 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do end context "and the user authenticates through a common SSO IDP" do - let(:authentication_method) { :sso } + let(:oidc_provider) { create(:oidc_provider, :token_exchange_capable) } + let(:storage) { create(:nextcloud_storage, :oidc_sso_enabled) } + let(:user_query_result) { Failure(Storages::Adapters::Results::Error.new(code: :error, source: self)) } - it "redirects to the project folder in the storage (the check is never performed)" do + current_user do + create(:user, authentication_provider: oidc_provider, member_with_permissions: { project => permissions }) + end + + it "renders an error message" do request expect(last_response).to have_http_status(:found) - expect(last_response.headers["Location"]).to eq("#{storage.host}index.php/f/123?openfile=1") + expect(last_response.headers["Location"]).to eq("http://test.host/projects/#{project.id}") + flash = Sessions::UserSession.last.data.dig("flash", "flashes") + expect(flash["error"]).to be_present end end end @@ -183,18 +184,17 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do end context "when the project folder has not been created yet" do - let(:project_storage) { create(:project_storage, storage:, project_folder_id: "", project_folder_mode:) } + let(:project_storage) { create(:project_storage, storage:, project_folder_id: nil, project_folder_mode:) } before do - allow(folder_create_service).to receive(:call) do - project_storage.update!(project_folder_id: "456") - ServiceResult.success + allow(Storages::NextcloudManagedFolderCreateService).to receive(:call) do + project_storage.update(project_folder_id: "456") && ServiceResult.success end end it "creates the project folder in the storage" do request - expect(folder_create_service).to have_received(:call) + expect(Storages::NextcloudManagedFolderCreateService).to have_received(:call) end it "redirects to the project folder in the storage" do @@ -205,7 +205,7 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do context "and when creation of the folder fails" do before do - allow(folder_create_service).to receive(:call).and_return( + allow(Storages::NextcloudManagedFolderCreateService).to receive(:call).and_return( ServiceResult.failure(errors: instance_double(ActiveModel::Errors, full_messages: ["Nope, sorry!"])) ) end @@ -225,7 +225,7 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do it "does not try to create the folder" do request - expect(folder_create_service).not_to have_received(:call) + expect(Storages::NextcloudManagedFolderCreateService).not_to have_received(:call) end it "redirects to the storage's file root" do @@ -237,15 +237,16 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do end context "when the user has no permission to access the project folder in the storage" do - let(:file_info_result) { ServiceResult.failure(errors: instance_double(Storages::StorageError, code: :forbidden)) } + let(:file_info_result) { Storages::Adapters::Results::Error.new(code: :forbidden, source: self) } before do - allow(folder_permissions_service).to receive(:call).and_return(ServiceResult.success) + allow(Storages::NextcloudManagedFolderPermissionsService).to receive(:call).and_return(ServiceResult.success) + Storages::Adapters::Registry.stub("nextcloud.queries.file_info", ->(*) { Failure(file_info_result) }) end it "updates the user's permissions on the remote folder" do request - expect(folder_permissions_service).to have_received(:call) + expect(Storages::NextcloudManagedFolderPermissionsService).to have_received(:call) end it "redirects to the project folder in the storage" do @@ -259,7 +260,7 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do it "does not try to update the permissions" do request - expect(folder_permissions_service).not_to have_received(:call) + expect(Storages::NextcloudManagedFolderPermissionsService).not_to have_received(:call) end it "redirects to the project folder in the storage (leaving final authorization to the storage)" do @@ -275,7 +276,7 @@ RSpec.describe "projects/:project_id/project_storages/:id/open" do # TODO: or should we avoid doing that? it "tries updating the user's permissions on the remote folder (ineffectively)" do request - expect(folder_permissions_service).to have_received(:call) + expect(Storages::NextcloudManagedFolderPermissionsService).to have_received(:call) end it "redirects to the storage's file root" do diff --git a/modules/storages/spec/requests/storages/project_settings/oauth_access_grant_flow_spec.rb b/modules/storages/spec/requests/storages/project_settings/oauth_access_grant_flow_spec.rb index b18886c5582..5c0215bc655 100644 --- a/modules/storages/spec/requests/storages/project_settings/oauth_access_grant_flow_spec.rb +++ b/modules/storages/spec/requests/storages/project_settings/oauth_access_grant_flow_spec.rb @@ -32,24 +32,26 @@ require "spec_helper" require_module_spec_helper RSpec.describe "GET /projects/:project_id/settings/project_storages/:id/oauth_access_grant", :webmock do - shared_let(:user) { create(:user, preferences: { time_zone: "Etc/UTC" }) } + let(:user) { create(:user, preferences: { time_zone: "Etc/UTC" }) } - shared_let(:role) do + let(:role) do create(:project_role, permissions: %i[manage_files_in_project oauth_access_grant select_project_modules edit_project]) end - shared_let(:storage) { create(:nextcloud_storage_with_complete_configuration) } + let(:storage) do + create(:nextcloud_storage_with_local_connection, :as_automatically_managed, oauth_client_token_user: user) + end - shared_let(:project) do + let(:project) do create(:project, name: "Project name without sequence", members: { user => role }, enabled_module_names: %i[storages work_package_tracking]) end - shared_let(:project_storage) { create(:project_storage, project:, storage:) } + let(:project_storage) { create(:project_storage, project:, storage:) } context "when user is not logged in" do it "requires login" do @@ -73,6 +75,9 @@ RSpec.describe "GET /projects/:project_id/settings/project_storages/:id/oauth_ac before do allow(SecureRandom).to receive(:uuid).and_call_original.ordered allow(SecureRandom).to receive(:uuid).and_return(nonce).ordered + Storages::Adapters::Registry + .stub("nextcloud.queries.user", + ->(_) { Failure(Storages::Adapters::Results::Error.new(code: :unauthorized, source: self)) }) end it "redirects to storage authorization_uri with oauth_state_* cookie set" do @@ -82,7 +87,7 @@ RSpec.describe "GET /projects/:project_id/settings/project_storages/:id/oauth_ac ) expect(last_response).to have_http_status(:found) expect(last_response.location).to eq( - "#{storage.host}/index.php/apps/oauth2/authorize?client_id=#{storage.oauth_client.client_id}&" \ + "#{storage.host}index.php/apps/oauth2/authorize?client_id=#{storage.oauth_client.client_id}&" \ "redirect_uri=#{redirect_uri}&response_type=code&state=#{nonce}" ) @@ -93,14 +98,14 @@ RSpec.describe "GET /projects/:project_id/settings/project_storages/:id/oauth_ac end context "when user is 'connected'" do - shared_let(:oauth_client_token) { create(:oauth_client_token, oauth_client: storage.oauth_client, user:) } + let(:oauth_client_token) { create(:oauth_client_token, oauth_client: storage.oauth_client, user:) } before do - Storages::Peripherals::Registry.stub("nextcloud.queries.user", ->(_) { ServiceResult.success }) + Storages::Adapters::Registry.stub("nextcloud.queries.user", ->(_) { Success() }) create(:remote_identity, user:, integration: storage) end - it "redirects to destination_url" do + it "redirects to destination_url", vcr: "nextcloud/user_query_success" do get oauth_access_grant_project_settings_project_storage_path( project_id: project_storage.project.id, id: project_storage diff --git a/modules/storages/spec/services/storages/create_folder_service_spec.rb b/modules/storages/spec/services/storages/create_folder_service_spec.rb index 84ea8b999bb..f465485854f 100644 --- a/modules/storages/spec/services/storages/create_folder_service_spec.rb +++ b/modules/storages/spec/services/storages/create_folder_service_spec.rb @@ -31,82 +31,80 @@ require "spec_helper" require_module_spec_helper -RSpec.describe Storages::CreateFolderService do - subject(:service) { described_class.call(storage:, user:, name:, parent_id:) } - - let(:user) { create(:admin) } - let(:name) { "TestFolderName" } - - context "when storage is nextcloud" do - let(:storage) { create(:nextcloud_storage_configured) } - let(:parent_id) { file_info.id } - +module Storages + RSpec.describe CreateFolderService do + let(:user) { create(:admin) } let(:file_info) do - Storages::StorageFileInfo.new( + Adapters::Results::StorageFileInfo.build( status: "OK", status_code: 200, id: SecureRandom.hex, name: "/", location: "/Path/To/Parent/Next" - ) + ).value! end - let(:create_folder_command) { class_double(Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) } + let(:name) { "TestFolderName" } + let(:auth_strategy) { Adapters::Registry["#{storage}.authentication.user_bound"].call(user, storage) } + + subject(:service) { described_class.new(storage) } before do - file_info_mock = class_double(Storages::Peripherals::StorageInteraction::Nextcloud::FileInfoQuery) - allow(file_info_mock).to receive(:call).with( - storage: storage, - auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - file_id: file_info.id - ).and_return(ServiceResult.success(result: file_info)) - Storages::Peripherals::Registry.stub("nextcloud.queries.file_info", file_info_mock) - - allow(create_folder_command).to receive(:call).and_return(ServiceResult.success) - Storages::Peripherals::Registry.stub("nextcloud.commands.create_folder", create_folder_command) + allow(StorageFileService).to receive(:call).with(storage:, user:, file_id: parent_id) + .and_return(ServiceResult.success(result: file_info)) end - it "calls the appropriate command with the expected parameters" do - service + context "when storage is nextcloud" do + let(:storage) { create(:nextcloud_storage) } + let(:parent_id) { file_info.id } - expect(create_folder_command).to have_received(:call).with( - storage:, - auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - folder_name: name, - parent_location: Storages::Peripherals::ParentFolder.new(file_info.location) - ).once - end - end + let(:create_folder_command) { class_double(Adapters::Providers::Nextcloud::Commands::CreateFolderCommand) } - context "when storage is one_drive" do - let(:storage) { create(:one_drive_storage) } - let(:parent_id) { file_info.id } + before do + allow(create_folder_command).to receive(:call).and_return(Success()) + Adapters::Registry.stub("nextcloud.commands.create_folder", create_folder_command) + end - let(:file_info) do - Storages::StorageFileInfo.new( - status: "OK", - status_code: 200, - id: "/Path/To/Parent/One", - name: "/" - ) + it "calls the appropriate command with the expected parameters" do + service.call(user:, name:, parent_id:) + + expect(create_folder_command).to have_received(:call).with( + storage:, + auth_strategy:, + input_data: Adapters::Input::CreateFolder.build(folder_name: name, parent_location: file_info.location).value! + ).once + end end - let(:create_folder_command) { class_double(Storages::Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) } + context "when storage is one_drive" do + let(:storage) { create(:one_drive_storage) } + let(:parent_id) { file_info.id } - before do - allow(create_folder_command).to receive(:call).and_return(ServiceResult.success) - Storages::Peripherals::Registry.stub("one_drive.commands.create_folder", create_folder_command) - end + let(:file_info) do + Adapters::Results::StorageFileInfo.build( + status: "OK", + status_code: 200, + id: "/Path/To/Parent/One", + name: "/" + ).value! + end - it "calls the appropriate command with the expected parameters" do - service + let(:create_folder_command) { class_double(Adapters::Providers::OneDrive::Commands::CreateFolderCommand) } - expect(create_folder_command).to have_received(:call).with( - storage:, - auth_strategy: instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - folder_name: name, - parent_location: Storages::Peripherals::ParentFolder.new(file_info.id) - ).once + before do + allow(create_folder_command).to receive(:call).and_return(Success()) + Adapters::Registry.stub("one_drive.commands.create_folder", create_folder_command) + end + + it "calls the appropriate command with the expected parameters" do + service.call(user:, name:, parent_id:) + + expect(create_folder_command).to have_received(:call).with( + storage:, + auth_strategy:, + input_data: Adapters::Input::CreateFolder.build(folder_name: name, parent_location: file_info.id).value! + ).once + end end end end diff --git a/modules/storages/spec/services/storages/file_links/copy_file_links_service_spec.rb b/modules/storages/spec/services/storages/file_links/copy_file_links_service_spec.rb index 02d7be58cbe..98fd56e8e47 100644 --- a/modules/storages/spec/services/storages/file_links/copy_file_links_service_spec.rb +++ b/modules/storages/spec/services/storages/file_links/copy_file_links_service_spec.rb @@ -66,13 +66,11 @@ RSpec.describe Storages::FileLinks::CopyFileLinksService, :webmock do end context "when AMPF is enabled" do - let(:files_info) { class_double(Storages::Peripherals::StorageInteraction::Nextcloud::FilesInfoQuery) } - let(:file_path_to_id) { class_double(Storages::Peripherals::StorageInteraction::Nextcloud::FilePathToIdMapQuery) } - let(:auth_strategy) do - Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy.new(key: :basic_auth) - end + let(:files_info) { class_double(Storages::Adapters::Providers::Nextcloud::Queries::FilesInfoQuery) } + let(:file_path_to_id) { class_double(Storages::Adapters::Providers::Nextcloud::Queries::FilePathToIdMapQuery) } + let(:auth_strategy) { Storages::Adapters::Registry["nextcloud.authentication.userless"].call } - let(:target_folder) { Storages::Peripherals::ParentFolder.new(target_storage.managed_project_folder_path) } + let(:target_folder) { target_storage.managed_project_folder_path } let(:remote_source_info) do source_links.map do |link| @@ -90,18 +88,16 @@ RSpec.describe Storages::FileLinks::CopyFileLinksService, :webmock do end before do - Storages::Peripherals::Registry.stub("nextcloud.queries.files_info", files_info) - Storages::Peripherals::Registry.stub("nextcloud.authentication.userless", -> { auth_strategy }) - Storages::Peripherals::Registry.stub("nextcloud.queries.file_path_to_id_map", file_path_to_id) + Storages::Adapters::Registry.stub("nextcloud.queries.files_info", files_info) + Storages::Adapters::Registry.stub("nextcloud.queries.file_path_to_id_map", file_path_to_id) - allow(Storages::Peripherals::ParentFolder).to receive(:new).with(target_storage.project_folder_location) - .and_return(target_folder) + info_data = Storages::Adapters::Input::FilesInfo.build(file_ids: source_links.map(&:origin_id)).value! + allow(files_info).to receive(:call).with(input_data: info_data, storage: source, auth_strategy:) + .and_return(Success(remote_source_info)) - allow(files_info).to receive(:call).with(file_ids: source_links.map(&:origin_id), storage: source, auth_strategy:) - .and_return(ServiceResult.success(result: remote_source_info)) - - allow(file_path_to_id).to receive(:call).with(storage: target, auth_strategy:, folder: target_folder) - .and_return(ServiceResult.success(result: path_to_ids)) + map_data = Storages::Adapters::Input::FilePathToIdMap.build(folder: target_folder).value! + allow(file_path_to_id).to receive(:call).with(storage: target, auth_strategy:, input_data: map_data) + .and_return(Success(path_to_ids)) end it "create links to the newly copied files" do diff --git a/modules/storages/spec/services/storages/file_links/file_link_sync_service_spec.rb b/modules/storages/spec/services/storages/file_links/file_link_sync_service_spec.rb index e4c607ae993..2a2d748e8c7 100644 --- a/modules/storages/spec/services/storages/file_links/file_link_sync_service_spec.rb +++ b/modules/storages/spec/services/storages/file_links/file_link_sync_service_spec.rb @@ -53,21 +53,21 @@ RSpec.describe Storages::FileLinkSyncService, type: :model do let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } before do - Storages::Peripherals::Registry - .stub("nextcloud.queries.files_info", ->(_) { ServiceResult.success(result: [file_info]) }) + Storages::Adapters::Registry + .stub("nextcloud.queries.files_info", ->(_) { Success([file_info]) }) end it "updates all origin_* fields" do expect(service.success).to be_truthy - expect(service.result.count).to be 1 - expect(service.result.first).to be_a Storages::FileLink + expect(service.result.count).to eq(1) + expect(service.result.first).to be_a(Storages::FileLink) - expect(service.result.first.origin_id).to eql file_info.id - expect(service.result.first.origin_created_at).to eql file_info.created_at - expect(service.result.first.origin_updated_at).to eql file_info.last_modified_at - expect(service.result.first.origin_mime_type).to eql file_info.mime_type - expect(service.result.first.origin_name).to eql file_info.name - expect(service.result.first.origin_created_by_name).to eql file_info.owner_name + expect(service.result.first.origin_id).to eq file_info.id + expect(service.result.first.origin_created_at).to eq file_info.created_at + expect(service.result.first.origin_updated_at).to eq file_info.last_modified_at + expect(service.result.first.origin_mime_type).to eq file_info.mime_type + expect(service.result.first.origin_name).to eq file_info.name + expect(service.result.first.origin_created_by_name).to eq file_info.owner_name end end @@ -76,13 +76,13 @@ RSpec.describe Storages::FileLinkSyncService, type: :model do let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } before do - Storages::Peripherals::Registry - .stub("nextcloud.queries.files_info", ->(_) { ServiceResult.success(result: [file_info]) }) + Storages::Adapters::Registry + .stub("nextcloud.queries.files_info", ->(_) { Success([file_info]) }) end it "returns a FileLink with #origin_status :not_allowed" do expect(service.success).to be_truthy - expect(service.result.first.origin_status).to be :view_not_allowed + expect(service.result.first.origin_status).to eq :view_not_allowed end end @@ -96,18 +96,18 @@ RSpec.describe Storages::FileLinkSyncService, type: :model do let(:file_links) { [file_link_one, file_link_two] } before do - Storages::Peripherals::Registry + Storages::Adapters::Registry .stub("nextcloud.queries.files_info", - ->(_) { ServiceResult.success(result: [file_info_one, file_info_two]) }) + ->(_) { Success([file_info_one, file_info_two]) }) end it "returns a successful result with two file links with different permissions" do expect(service.success).to be_truthy - expect(service.result.count).to be 2 - expect(service.result[0].origin_id).to eql file_info_one.id - expect(service.result[1].origin_id).to eql file_info_two.id - expect(service.result[0].origin_status).to be :view_allowed - expect(service.result[1].origin_status).to be :view_not_allowed + expect(service.result.count).to eq 2 + expect(service.result[0].origin_id).to eq file_info_one.id + expect(service.result[1].origin_id).to eq file_info_two.id + expect(service.result[0].origin_status).to eq :view_allowed + expect(service.result[1].origin_status).to eq :view_not_allowed end end @@ -116,15 +116,15 @@ RSpec.describe Storages::FileLinkSyncService, type: :model do let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } before do - Storages::Peripherals::Registry - .stub("nextcloud.queries.files_info", ->(_) { ServiceResult.success(result: [file_info]) }) + Storages::Adapters::Registry + .stub("nextcloud.queries.files_info", ->(_) { Success([file_info]) }) end it "returns the file link with a status set to :not_found" do expect(service.success).to be_truthy - expect(service.result.count).to be 1 - expect(Storages::FileLink.count).to be 1 - expect(service.result.first.origin_status).to be :not_found + expect(service.result.count).to eq 1 + expect(Storages::FileLink.count).to eq 1 + expect(service.result.first.origin_status).to eq :not_found end end @@ -133,28 +133,28 @@ RSpec.describe Storages::FileLinkSyncService, type: :model do let(:file_link_one) { create(:file_link, origin_id: file_info.id, storage: storage_one, container: work_package) } before do - Storages::Peripherals::Registry - .stub("nextcloud.queries.files_info", ->(_) { ServiceResult.success(result: [file_info]) }) + Storages::Adapters::Registry + .stub("nextcloud.queries.files_info", ->(_) { Success([file_info]) }) end it "returns the file link with a status set to :error" do expect(service.success).to be_truthy - expect(service.result.count).to be 1 - expect(Storages::FileLink.count).to be 1 - expect(service.result.first.origin_status).to be :error + expect(service.result.count).to eq 1 + expect(Storages::FileLink.count).to eq 1 + expect(service.result.first.origin_status).to eq :error end end context "with files_info_query failing" do before do - Storages::Peripherals::Registry + Storages::Adapters::Registry .stub("nextcloud.queries.files_info", - ->(_) { ServiceResult.failure(result: :error, errors: Storages::StorageError.new(code: :error)) }) + ->(_) { Failure(Storages::Adapters::Results::Error.new(code: :error, source: "Specs")) }) end it "leaves the list of file_links unchanged with permissions = :error" do expect(service.success).to be_truthy - expect(service.result.first.origin_status).to be :error + expect(service.result.first.origin_status).to eq :error end end end diff --git a/modules/storages/spec/services/storages/managed_folder_sync_service_spec.rb b/modules/storages/spec/services/storages/managed_folder_sync_service_spec.rb index f11150d8406..bdd92a1395e 100644 --- a/modules/storages/spec/services/storages/managed_folder_sync_service_spec.rb +++ b/modules/storages/spec/services/storages/managed_folder_sync_service_spec.rb @@ -39,18 +39,20 @@ RSpec.describe Storages::ManagedFolderSyncService do class_double(Storages::NextcloudManagedFolderPermissionsService, call: ServiceResult.success) end + # TODO: This masks missing keys. + # We may need to figure out a better way to write this these tests - 2025-05-08 @mereghost before do - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("nextcloud.services.folder_create") + allow(Storages::Adapters::Registry).to receive(:resolve) + .with("nextcloud.services.upkeep_managed_folders") .and_return(folder_create_service) - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("one_drive.services.folder_create") + allow(Storages::Adapters::Registry).to receive(:resolve) + .with("one_drive.services.upkeep_managed_folders") .and_return(folder_create_service) - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("nextcloud.services.folder_permissions") + allow(Storages::Adapters::Registry).to receive(:resolve) + .with("nextcloud.services.upkeep_managed_folder_permissions") .and_return(folder_permissions_service) - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("one_drive.services.folder_permissions") + allow(Storages::Adapters::Registry).to receive(:resolve) + .with("one_drive.services.upkeep_managed_folder_permissions") .and_return(folder_permissions_service) end @@ -69,12 +71,13 @@ RSpec.describe Storages::ManagedFolderSyncService do context "when the storage is a Nextcloud storage" do it "uses the Nextcloud folder create service" do call - expect(Storages::Peripherals::Registry).to have_received(:resolve).with("nextcloud.services.folder_create") + expect(Storages::Adapters::Registry).to have_received(:resolve).with("nextcloud.services.upkeep_managed_folders") end it "calls the Nextcloud folder permissions service" do call - expect(Storages::Peripherals::Registry).to have_received(:resolve).with("nextcloud.services.folder_permissions") + expect(Storages::Adapters::Registry) + .to have_received(:resolve).with("nextcloud.services.upkeep_managed_folder_permissions") end end @@ -83,12 +86,13 @@ RSpec.describe Storages::ManagedFolderSyncService do it "calls the OneDrive folder create service" do call - expect(Storages::Peripherals::Registry).to have_received(:resolve).with("one_drive.services.folder_create") + expect(Storages::Adapters::Registry).to have_received(:resolve).with("one_drive.services.upkeep_managed_folders") end it "calls the OneDrive folder permissions service" do call - expect(Storages::Peripherals::Registry).to have_received(:resolve).with("one_drive.services.folder_permissions") + expect(Storages::Adapters::Registry) + .to have_received(:resolve).with("one_drive.services.upkeep_managed_folder_permissions") end end diff --git a/modules/storages/spec/services/storages/nextcloud_managed_folder_create_service_spec.rb b/modules/storages/spec/services/storages/nextcloud_managed_folder_create_service_spec.rb index 9a0d6133411..4be31d10a5c 100644 --- a/modules/storages/spec/services/storages/nextcloud_managed_folder_create_service_spec.rb +++ b/modules/storages/spec/services/storages/nextcloud_managed_folder_create_service_spec.rb @@ -31,8 +31,25 @@ require "spec_helper" require_module_spec_helper +RSpec::Matchers.define_negated_matcher :not_change, :change + module Storages + FakeProject = Data.define(:id, :name) + + class TestIdentifier < Adapters::Providers::Nextcloud::ManagedFolderIdentifier + def initialize(project_storage) + super + @project = FakeProject.new(-273, project_storage.project.name) + end + end + RSpec.describe NextcloudManagedFolderCreateService, :webmock do + before do + Adapters::Registry.stub("nextcloud.models.managed_folder_identifier", TestIdentifier) + end + + after { delete_created_folders } + describe "#call" do subject(:service) { described_class.new(storage:) } @@ -43,7 +60,7 @@ module Storages shared_let(:single_project_user) { create(:user) } shared_let(:oidc_user) { create(:user, authentication_provider: oidc_provider) } shared_let(:oidc_admin) { create(:admin, authentication_provider: oidc_provider) } - shared_let(:storage) { create(:nextcloud_storage_with_complete_configuration, :as_automatically_managed) } + shared_let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed) } shared_let(:remote_identities) do [create(:remote_identity, @@ -76,340 +93,208 @@ module Storages shared_let(:non_member_role) { create(:non_member, permissions: ["read_files"]) } shared_let(:ordinary_role) { create(:project_role, permissions: %w[read_files write_files]) } - shared_let(:public_project) { create(:public_project, name: "PUBLIC PROJECT") } - shared_let(:inactive_project) do - create(:project, :archived, name: "INACTIVE PROJECT", members: { multiple_projects_user => ordinary_role }) - end shared_let(:project) do - create(:project, - name: "[Sample] Project Name / Ehüu ///", - members: { multiple_projects_user => ordinary_role, single_project_user => ordinary_role }) + create(:project, name: "[Sample] Project Name / Ehuu", + members: { multiple_projects_user => ordinary_role, single_project_user => ordinary_role }) end - shared_let(:renamed_project) do - create(:project, - name: "Renamed Project #23", - members: { multiple_projects_user => ordinary_role }) + shared_let(:project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", storage:, project:) end - let!(:public_storage) { create(:project_storage, :as_automatically_managed, storage:, project: public_project) } - let!(:project_storage) { create(:project_storage, :as_automatically_managed, storage:, project:) } - - let!(:inactive_storage) do - create(:project_storage, :as_automatically_managed, :with_historical_data, - storage:, project: inactive_project, project_folder_id: "12345") + shared_let(:disallowed_chars_project) do + create(:project, name: '<=o=> | "Jedi" Project Folder ///', members: { multiple_projects_user => ordinary_role }) end - let!(:renamed_storage) do - create(:project_storage, :as_automatically_managed, :with_historical_data, - storage:, project: renamed_project, project_folder_id: "9001") + shared_let(:disallowed_chars_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", + project: disallowed_chars_project, storage:) end - let(:file_path_to_id_map) { class_double(Peripherals::StorageInteraction::Nextcloud::FilePathToIdMapQuery) } - let(:rename_file) { class_double(Peripherals::StorageInteraction::Nextcloud::RenameFileCommand) } - let(:set_permissions) { class_double(Peripherals::StorageInteraction::Nextcloud::SetPermissionsCommand) } - let(:create_folder) { class_double(Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) } - let(:auth_strategy) { Peripherals::StorageInteraction::AuthenticationStrategies::Strategy.new(:basic_auth) } - - let(:root_folder_id) { "root_folder_id" } - let(:file_path_to_id_map_result) do - inactive_storage_path = inactive_storage.managed_project_folder_path.chomp("/") - - ServiceResult.success( - result: { - "/OpenProject" => StorageFileId.new(root_folder_id), - inactive_storage_path => StorageFileId.new(inactive_storage.project_folder_id), - "/OpenProject/Another Name for this Project" => StorageFileId.new(renamed_storage.project_folder_id) - } - ) + shared_let(:inactive_project) do + create(:project, name: "INACTIVE PROJECT! f0r r34lz", active: false, members: { multiple_projects_user => ordinary_role }) + end + shared_let(:inactive_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: inactive_project, storage:) end - let(:root_permissions_result) { ServiceResult.success } - let(:root_permission_input) do - build_input_data( - "root_folder_id", - [ - { user_id: "OpenProject", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { group_id: "OpenProject", permissions: %i[read_files] } - ] - ) + shared_let(:public_project) { create(:public_project, name: "PUBLIC PROJECT", active: true) } + shared_let(:public_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: public_project, storage:) end - let(:projects_folder_permissions) { build_project_folder_permission_input } - - let(:rename_file_result) do - StorageFile.new(id: renamed_storage.project_folder_id, name: renamed_storage.managed_project_folder_name, - location: renamed_storage.managed_project_folder_path) + shared_let(:unmanaged_project) do + create(:project, name: "Non Managed Project", active: true, members: { multiple_projects_user => ordinary_role }) + end + shared_let(:unmanaged_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "manual", project: unmanaged_project, storage:) end - let(:parent_location) { Peripherals::ParentFolder.new("/") } - let(:create_folder_result) { build_create_folder_result } - - before do - Peripherals::Registry.stub("nextcloud.queries.file_path_to_id_map", file_path_to_id_map) - Peripherals::Registry.stub("nextcloud.commands.create_folder", create_folder) - Peripherals::Registry.stub("nextcloud.commands.rename_file", rename_file) - Peripherals::Registry.stub("nextcloud.commands.set_permissions", set_permissions) - Peripherals::Registry.stub("nextcloud.authentication.userless", -> { auth_strategy }) - - # We arent using ParentFolder nor AuthStrategies on FileIds - folder = Peripherals::ParentFolder.new(storage.group) - allow(file_path_to_id_map).to receive(:call).with(storage:, auth_strategy:, folder:, depth: 1) - .and_return(file_path_to_id_map_result) - - # Setting the Group Permissions - allow(set_permissions).to receive(:call).with(storage:, auth_strategy:, input_data: root_permission_input) - .and_return(root_permissions_result) - - # Creating folders - allow(create_folder).to receive(:call).with(storage:, auth_strategy:, parent_location:, - folder_name: project_storage.managed_project_folder_path) - .and_return(create_folder_result[project_storage.managed_project_folder_name]) - - allow(create_folder).to receive(:call).with(storage:, auth_strategy:, parent_location:, - folder_name: public_storage.managed_project_folder_path) - .and_return(create_folder_result[public_storage.managed_project_folder_name]) - - # Renaming folders - allow(rename_file).to receive(:call).with(storage:, auth_strategy:, file_id: renamed_storage.project_folder_id, - name: renamed_storage.managed_project_folder_name) - .and_return(ServiceResult.success(result: rename_file_result)) - - # Project Permissions + Hiding Projects - projects_folder_permissions.each do |input_data| - allow(set_permissions).to receive(:call).with(storage:, auth_strategy:, input_data:) - .and_return(ServiceResult.success) - end - end - - it "is a success" do - expect(service.call).to be_success - end - - it "updates the project storage with the remote folder id" do - expect { service.call }.to change { project_storage.reload.project_folder_id } - .from(nil).to("normal_project_id") - end - - context "when a project is renamed" do - let(:file_path_to_id_map_result) do - ServiceResult.success( - result: { - "/OpenProject" => StorageFileId.new(root_folder_id), - "/OpenProject/OBVIOUSLY NON RENAMED" => StorageFileId.new(renamed_storage.project_folder_id) - } - ) + describe "Remote Folder Creation" do + it "updates the project folder id for all active automatically managed projects", + vcr: "nextcloud/managed_folder_create_service" do + expect { service.call }.to change { disallowed_chars_project_storage.reload.project_folder_id } + .from(nil).to(String) + .and(change { project_storage.reload.project_folder_id }.from(nil).to(String)) + .and(change { public_project_storage.reload.project_folder_id }.from(nil).to(String)) + .and(not_change { inactive_project_storage.reload.project_folder_id }) + .and(not_change { unmanaged_project_storage.reload.project_folder_id }) end - let(:group_users_result) do - ServiceResult.success(result: %w[OpenProject admin single_project_user multiple_projects_user]) + it "adds a record to the LastProjectFolder for each new folder", + vcr: "nextcloud/managed_folder_create_service" do + scope = ->(project_storage) { LastProjectFolder.where(project_storage:).last } + + expect { service.call }.to not_change { scope[unmanaged_project_storage].reload.origin_folder_id } + .and(not_change { scope[inactive_project_storage].reload.origin_folder_id }) + + expect(scope[project_storage].origin_folder_id).to eq(project_storage.reload.project_folder_id) + expect(scope[public_project_storage].origin_folder_id).to eq(public_project_storage.reload.project_folder_id) + expect(scope[disallowed_chars_project_storage].origin_folder_id) + .to eq(disallowed_chars_project_storage.reload.project_folder_id) end - before { ProjectStorage.where.not(id: renamed_storage.id).delete_all } - - it "is a success" do - expect(service.call).to be_success - end - - it "requests to rename the folder to the new managed folder name" do - service.call - expect(rename_file).to have_received(:call) - .with(storage:, - auth_strategy:, - file_id: renamed_storage.project_folder_id, - name: renamed_storage.managed_project_folder_name).once - end - - it "does not change the project_folder_id after the rename" do - expect { service.call }.not_to change { renamed_storage.reload.project_folder_id } - end - end - - context "when creating a folder for a project that with trailing slashes in its name" do - it "replaces the offending characters" do + it "creates the remote folders for all projects with automatically managed folders enabled", + vcr: "nextcloud/managed_folder_create_service" do service.call - expect(create_folder).to have_received(:call) - .with(storage:, auth_strategy:, parent_location: Peripherals::ParentFolder.new("/"), - folder_name: "/OpenProject/[Sample] Project Name | Ehüu ||| (#{project.id})/").once + [project_storage, disallowed_chars_project_storage, public_project_storage].each do |proj_storage| + expect(project_folder_info(proj_storage)).to be_success + end end - it "is a success" do - expect(service.call).to be_success - end + it "makes sure that the last_project_folder.origin_folder_id match the current project_folder_id", + vcr: "nextcloud/managed_folder_create_service" do + service.call - it "adds a new entry on historical data" do - expect { service.call }.to change { LastProjectFolder.where(project_storage:).count }.by(1) + [project_storage, disallowed_chars_project_storage, public_project_storage].each do |proj_storage| + proj_storage.reload + the_real_last_project_folder = proj_storage.last_project_folders.last + + expect(proj_storage.project_folder_id).to eq(the_real_last_project_folder.origin_folder_id) + end end end - context "with an archived project" do - it "is a success" do - expect(service.call).to be_success + it "renames an already existing project folder", vcr: "nextcloud/managed_folder_create_service_rename_folder" do + create_folder_for(disallowed_chars_project_storage, "Old Jedi Project").bind do |original_folder| + disallowed_chars_project_storage.update(project_folder_id: original_folder.id) end - it "hides the project folder" do - input_data = build_project_folder_permission_input[0] - service.call + service_result = service.call + expect(service_result).to be_success + expect(service_result.errors).to be_empty - expect(set_permissions).to have_received(:call).with(storage:, auth_strategy:, input_data:).once + result = project_folder_info(disallowed_chars_project_storage.reload).value! + expect(result.name).to match(%r{<=o=> | "Jedi" Project Folder ||| \(-273\)}) + end + + it "hides (removes all permissions) from inactive project folders", + vcr: "nextcloud/managed_folder_create_service_hide_inactive" do + create_folder_for(inactive_project_storage).bind do |original_folder| + inactive_project_storage.update(project_folder_id: original_folder.id) + + # add_users_to_group(%w[anakin leia luke]) + set_permissions_on(original_folder.id, + [{ user_id: "anakin", permissions: [:read_files] }, + { user_id: "luke", permissions: [:write_files] }]) end + + result = service.call + + expect(result).to be_success + expect(result.errors).to be_empty + users = remote_permissions_for(inactive_project_storage).map { |hash| hash[:user_id] } + + # Group, User + expect(users).to contain_exactly("OpenProject", "OpenProject") end describe "error handling" do let(:error_prefix) { "services.errors.models.nextcloud_sync_service" } - context "when the initial fetch of remote folders fails" do - let(:file_path_to_id_map_result) do - errors = storage_error(:unauthorized, - "error body", - Peripherals::StorageInteraction::Nextcloud::FilePathToIdMapQuery) - ServiceResult.failure(result: :unauthorized, errors:) - end + before { allow(Rails.logger).to receive_messages(%i[error warn]) } - it "logs an error" do - allow(Rails.logger).to receive(:error).and_call_original + context "when the initial fetch of remote folders fails" do + it "logs an error", vcr: "nextcloud/sync_service_root_read_failure" do service.call expect(Rails.logger) - .to have_received(:error).with(error_code: :unauthorized, message: "TESTING", - folder: "OpenProject", data: "error body") + .to have_received(:error).with(error_code: :unauthorized, + data: { body: /Server Error/, status: Integer }, + group_folder: storage.group_folder, username: storage.username) end - it "is a failure" do + it "is a failure", vcr: "nextcloud/sync_service_root_read_failure" do expect(service.call).to be_failure end - it "adds to the services errors" do + it "adds to the services errors", vcr: "nextcloud/sync_service_root_read_failure" do result = service.call expect(result.errors.size).to eq(1) expect(result.errors[:base]).to contain_exactly(I18n.t("#{error_prefix}.unauthorized")) end - - it "interrupts the flow" do - service.call - [create_folder, rename_file, set_permissions].each do |command| - expect(command).not_to have_received(:call) - end - end end context "when we fail to set the root folder permissions" do - let(:root_permissions_result) do - errors = storage_error(:error, "error body", Peripherals::StorageInteraction::Nextcloud::SetPermissionsCommand) - ServiceResult.failure(result: :unauthorized, errors:) + let(:error) { Adapters::Results::Error.new(code: :error, source: self) } + + before do + set_permissions_class_double = class_double(Adapters::Providers::Nextcloud::Commands::SetPermissionsCommand) + set_permissions_double = instance_double(Adapters::Providers::Nextcloud::Commands::SetPermissionsCommand) + + allow(set_permissions_class_double).to receive(:new).with(storage).and_return(set_permissions_double) + allow(set_permissions_double).to receive(:call).and_return(Failure(error)) + Adapters::Registry.stub("nextcloud.commands.set_permissions", set_permissions_class_double) end - it "logs an error" do - allow(Rails.logger).to receive(:error).and_call_original + it "logs an error", vcr: "nextcloud/managed_folder_create_service" do service.call - expect(Rails.logger).to have_received(:error) .with(error_code: :error, - message: "TESTING", - folder: "root", - data: "error body", - root_folder_id: "root_folder_id") + data: "", + group: storage.group, + username: storage.username) end - it "is a failure" do + it "is a failure", vcr: "nextcloud/managed_folder_create_service" do expect(service.call).to be_failure end - it "adds to the services errors" do + it "adds to the services errors", vcr: "nextcloud/managed_folder_create_service" do result = service.call expect(result.errors.size).to eq(1) expect(result.errors[:base]).to contain_exactly(I18n.t("#{error_prefix}.error")) end - - it "interrupts the flow" do - service.call - expect(set_permissions).to have_received(:call).once - - [create_folder, rename_file].each do |command| - expect(command).not_to have_received(:call) - end - end end context "when creating folders fails" do - let(:create_folder_result) do - errors = storage_error(:conflict, "error body", Peripherals::StorageInteraction::Nextcloud::CreateFolderCommand) + it "doesn't update the project_storage", vcr: "nextcloud/sync_service_creation_fail" do + already_existing_folder = create_folder_for(project_storage).value! + result = nil - build_create_folder_result - .merge(project_storage.managed_project_folder_name => ServiceResult.failure(result: :conflict, errors:)) - end + expect { result = service.call }.not_to change(project_storage, :project_folder_id) - it "logs an error" do - allow(Rails.logger).to receive(:error).and_call_original - service.call - - expect(Rails.logger).to have_received(:error) - .with(error_code: :conflict, message: "TESTING", - folder_name: project_storage.managed_project_folder_path, data: "error body") - end - - it "is a success" do - # TODO: why is this a success? Bug or intention? - expect(service.call).to be_success - end - - it "adds to the services errors" do - result = service.call - - expect(result.errors.size).to eq(1) + expect(result).to be_failure expect(result.errors[:create_folder]) - .to contain_exactly(I18n.t("#{error_prefix}.attributes.create_folder.conflict", - folder_name: project_storage.managed_project_folder_path, parent_location: "/")) + .to match_array(I18n.t("#{error_prefix}.attributes.create_folder.conflict", + folder_name: project_storage.managed_project_folder_path, + parent_location: "/")) + ensure + delete_folder(already_existing_folder.id) end - it "does not interrupt the flow" do - commands = [file_path_to_id_map, set_permissions, create_folder, rename_file] - service.call - expect(commands).to all(have_received(:call).at_least(:once)) - end - end - end - - context "when passing a project storage scope" do - subject(:service) { described_class.new(storage:, project_storages_scope:) } - let(:project_storages_scope) { ProjectStorage.where(id: project_storage.id) } - - it "creates the remote folder for the project storage inside the scope" do - service.call - - expect(create_folder).to have_received(:call).with(storage:, auth_strategy:, parent_location:, - folder_name: project_storage.managed_project_folder_path) - end - - it "creates no remote folder for the public storage outside the scope" do - service.call - - expect(create_folder).not_to have_received(:call).with(storage:, auth_strategy:, parent_location:, - folder_name: public_storage.managed_project_folder_path) - end - - it "does not hide remote folders (full list of storages not known)" do - service.call - - # We expect that the only call is made to ensure root folder permissions, but no other calls (to hide folders) are made. - # Using a positive expectation avoids wrongly succeeding specs, once method signatures change. - expect(set_permissions).to have_received(:call).once - expect(set_permissions).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes(file_id: root_folder_id) - ) - end - - context "when project storage within scope is non-automatically managed" do - before do - project_storage.update!(project_folder_mode: "manual") - end - - it "creates no remote folders" do + it "logs the occurrence", vcr: "nextcloud/sync_service_creation_fail" do + already_existing_folder = create_folder_for(project_storage).value! service.call - expect(create_folder).not_to have_received(:call) + expect(Rails.logger) + .to have_received(:error) + .with(folder_name: project_storage.managed_project_folder_path, + error_code: :conflict, + parent_location: "/", + data: { body: String, status: 405 }) + ensure + delete_folder(already_existing_folder.id) end end end @@ -417,69 +302,97 @@ module Storages private - def storage_error(code, data, source) - data = StorageErrorData.new(source:, payload: data) - StorageError.new(code:, log_message: "TESTING", data:) + def set_permissions_on(file_id, user_permissions) + Adapters::Input::SetPermissions.build(user_permissions:, file_id:).bind do |input_data| + Adapters::Registry["nextcloud.commands.set_permissions"].call(storage:, auth_strategy:, input_data:) + end end - def build_create_folder_result - { - public_storage.managed_project_folder_name => - ServiceResult.success(result: StorageFile.new(id: "public_id", - name: public_storage.managed_project_folder_name)), - project_storage.managed_project_folder_name => - ServiceResult.success(result: StorageFile.new(id: "normal_project_id", - name: project_storage.managed_project_folder_name)) - } + def remote_permissions_for(project_storage) + Adapters::Authentication[auth_strategy].call(storage:) do |http| + request_url = UrlBuilder.url(storage.uri, "remote.php/dav/files", storage.username, + project_storage.managed_project_folder_path) + response = http.request(:propfind, request_url, xml: permission_request_body) + parse_acl_xml response.body.to_s + end end - def build_project_folder_permission_input - [ - build_input_data( - inactive_storage.project_folder_id, - [ - { user_id: "OpenProject", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { group_id: "OpenProject", permissions: [] } - ] - ), - build_input_data( - "public_id", - [ - { user_id: "OpenProject", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "admin", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "oidc_admin", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "multiple_projects_user", permissions: %i[read_files] }, - { user_id: "oidc_user", permissions: %i[read_files] }, - { user_id: "single_project_user", permissions: %i[read_files] }, - { group_id: "OpenProject", permissions: [] } - ] - ), - build_input_data( - "normal_project_id", - [ - { user_id: "OpenProject", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "admin", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "oidc_admin", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "multiple_projects_user", permissions: %i[read_files write_files] }, - { user_id: "single_project_user", permissions: %i[read_files write_files] }, - { group_id: "OpenProject", permissions: [] } - ] - ), - build_input_data( - renamed_storage.project_folder_id, - [ - { user_id: "OpenProject", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "admin", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "oidc_admin", permissions: OpenProject::Storages::Engine.external_file_permissions }, - { user_id: "multiple_projects_user", permissions: %i[read_files write_files] }, - { group_id: "OpenProject", permissions: [] } - ] - ) - ] + def permission_request_body + Nokogiri::XML::Builder.new do |xml| + xml["d"].propfind( + "xmlns:d" => "DAV:", + "xmlns:nc" => "http://nextcloud.org/ns" + ) do + xml["d"].prop do + xml["nc"].send(:"acl-list") + end + end + end.to_xml end - def build_input_data(file_id, user_permissions) - Peripherals::StorageInteraction::Inputs::SetPermissions.build(file_id:, user_permissions:).value! + def parse_acl_xml(xml) + found_code = "d:status[text() = 'HTTP/1.1 200 OK']" + not_found_code = "d:status[text() = 'HTTP/1.1 404 Not Found']" + happy_path = "/d:multistatus/d:response/d:propstat[#{found_code}]/d:prop/nc:acl-list" + not_found_path = "/d:multistatus/d:response/d:propstat[#{not_found_code}]/d:prop" + + if Nokogiri::XML(xml).xpath(not_found_path).children.map(&:name).include?("acl-list") + [] + else + Nokogiri::XML(xml).xpath(happy_path).children.map do |acl| + acl.children.each_with_object({ user_id: "", permissions: [] }) do |entry, agg| + agg[:user_id] = entry.text if entry.name == "acl-mapping-id" + agg[:permissions] = translate_mask_to_permissions(entry.text.to_i) if entry.name == "acl-permissions" + end + end + end + end + + def translate_mask_to_permissions(number) + Adapters::Providers::Nextcloud::Commands::SetPermissionsCommand::PERMISSIONS_MAP + .each_with_object([]) { |(permission, mask), list| list << permission if number & mask == mask } + end + + def create_folder_for(project_storage, folder_override = nil) + folder_name = folder_override || project_storage.managed_project_folder_name + Adapters::Input::CreateFolder.build(parent_location: storage.group_folder, folder_name:).bind do |input_data| + Adapters::Registry["nextcloud.commands.create_folder"].call(storage:, auth_strategy:, input_data:) + end + end + + def original_folders + root_folder_contents.fmap do |storage_files| + storage_files.files.find { |file| file.id == project_storage.project_folder_id } + end + end + + def project_folder_info(project_storage) + root_folder_contents.fmap do |storage_files| + storage_files.files.find { |file| file.id == project_storage.reload.project_folder_id } + end + end + + def root_folder_contents + Adapters::Input::Files.build(folder: storage.group_folder).bind do |input_data| + Adapters::Registry["nextcloud.queries.files"].call(storage:, auth_strategy:, input_data:) + end + end + + def delete_created_folders + storage.project_storages.automatic + .where(storage:) + .where.not(project_folder_id: nil) + .find_each { |project_storage| delete_folder(project_storage.managed_project_folder_path.chop) } + end + + def delete_folder(item_id) + Adapters::Input::DeleteFolder.build(location: item_id).bind do |input_data| + Adapters::Registry["nextcloud.commands.delete_folder"].call(storage:, auth_strategy:, input_data:) + end + end + + def auth_strategy + Adapters::Registry["nextcloud.authentication.userless"].call end end end diff --git a/modules/storages/spec/services/storages/nextcloud_managed_folder_permissions_service_spec.rb b/modules/storages/spec/services/storages/nextcloud_managed_folder_permissions_service_spec.rb index 6040e12822b..f098cdc9256 100644 --- a/modules/storages/spec/services/storages/nextcloud_managed_folder_permissions_service_spec.rb +++ b/modules/storages/spec/services/storages/nextcloud_managed_folder_permissions_service_spec.rb @@ -31,421 +31,231 @@ require "spec_helper" require_module_spec_helper -RSpec.describe Storages::NextcloudManagedFolderPermissionsService, :webmock do - describe "#call" do - subject(:service_call) { described_class.call(storage:) } +module Storages + FakeProject = Data.define(:id, :name) - let(:storage) { create(:nextcloud_storage_with_complete_configuration, :as_automatically_managed) } - let(:project) { create(:project, members: { project_user => role, user_without_remote_identity => role }) } - let(:role) { create(:project_role, permissions: %i[read_files]) } - let(:non_member_permissions) { %i[read_files view_work_packages] } - - # rubocop:disable RSpec/VerifiedDoubles - let(:set_permissions_service) { double(call: ServiceResult.success) } - let(:add_user_to_group_service) { double(call: ServiceResult.success) } - let(:remove_user_from_group_service) { double(call: ServiceResult.success) } - let(:group_users_service) { double(call: ServiceResult.success(result: ["unrelated-remote-identity"])) } - let(:auth_strategy) { Object.new } - # rubocop:enable RSpec/VerifiedDoubles - - let!(:project_storage) { create(:project_storage, :as_automatically_managed, project:, storage:, project_folder_id: "123") } - let!(:non_project_user) { create(:user) } - let!(:project_user) { create(:user) } - let!(:user_without_remote_identity) { create(:user) } - let!(:admin_user) { create(:admin) } - let!(:non_project_remote_identity) { create(:remote_identity, integration: storage, user: non_project_user) } - let!(:project_remote_identity) { create(:remote_identity, integration: storage, user: project_user) } - let!(:admin_remote_identity) { create(:remote_identity, integration: storage, user: admin_user) } - - before do - ProjectRole.non_member.update!(permissions: non_member_permissions) - - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("nextcloud.commands.set_permissions") - .and_return(set_permissions_service) - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("nextcloud.commands.add_user_to_group") - .and_return(add_user_to_group_service) - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("nextcloud.commands.remove_user_from_group") - .and_return(remove_user_from_group_service) - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("nextcloud.queries.group_users") - .and_return(group_users_service) - allow(Storages::Peripherals::Registry).to receive(:resolve) - .with("nextcloud.authentication.userless") - .and_return(-> { auth_strategy }) - end - - it { is_expected.to be_success } - - it "has no errors" do - expect(service_call.errors).to be_empty - end - - it "adds users with a remote identity to the remote group", :aggregate_failures do - service_call - - expect(add_user_to_group_service).to have_received(:call).exactly(3).times - expect(add_user_to_group_service).to have_received(:call) - .with(storage:, auth_strategy:, group: storage.group, user: project_remote_identity.origin_user_id) - expect(add_user_to_group_service).to have_received(:call) - .with(storage:, auth_strategy:, group: storage.group, user: admin_remote_identity.origin_user_id) - expect(add_user_to_group_service).to have_received(:call) - .with(storage:, auth_strategy:, group: storage.group, user: non_project_remote_identity.origin_user_id) - end - - it "removes the unrelated user from the remote group" do - service_call - - expect(remove_user_from_group_service).to have_received(:call) - .with(storage:, auth_strategy:, group: storage.group, user: "unrelated-remote-identity") - end - - it "grants permissions to project users" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes( - file_id: "123", user_permissions: array_including( - { user_id: project_remote_identity.origin_user_id, permissions: [:read_files] } - ) - ) - ) - end - - it "grants permissions to admin users" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes( - file_id: "123", user_permissions: array_including( - { - user_id: admin_remote_identity.origin_user_id, - permissions: %i[read_files write_files create_files delete_files share_files] - } - ) - ) - ) - end - - it "grants permissions to the system-level OpenProject user" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes( - file_id: "123", user_permissions: array_including( - { user_id: "OpenProject", permissions: %i[read_files write_files create_files delete_files share_files] } - ) - ) - ) - end - - it "grants no permissions to non-project users" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).not_to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes( - file_id: "123", user_permissions: array_including(hash_including(user_id: non_project_remote_identity.origin_user_id)) - ) - ) - end - - context "when the project storage is not automatic" do - let!(:project_storage) { create(:project_storage, project:, storage:, project_folder_mode: "manual") } - - it { is_expected.to be_success } - - it "has no errors" do - expect(service_call.errors).to be_empty - end - - it "updates the remote group regardless" do - service_call - - expect(add_user_to_group_service).to have_received(:call).exactly(3).times - end - - it "does not touch permissions" do - service_call - - expect(set_permissions_service).not_to have_received(:call) - end - end - - context "when the project is public" do - before do - project.update!(public: true) - end - - it "grants permissions to non-project users as well" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes( - file_id: "123", user_permissions: array_including( - { user_id: non_project_remote_identity.origin_user_id, permissions: [:read_files] } - ) - ) - ) - end - - context "and when the non-member permissions don't include file access" do - let(:non_member_permissions) { %i[view_work_packages] } - - it "grants no permissions to non-project users" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).not_to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes( - file_id: "123", - user_permissions: array_including(hash_including(user_id: non_project_remote_identity.origin_user_id)) - ) - ) - end - end - end - - context "when the project storage's project is not active" do - before do - project.update!(active: false) - end - - it { is_expected.to be_success } - - it "has no errors" do - expect(service_call.errors).to be_empty - end - - it "does not touch permissions" do - service_call - - expect(set_permissions_service).not_to have_received(:call) - end - end - - context "when all users are part of the remote group" do - let(:group_users_service) do - double(call: ServiceResult.success(result: [ # rubocop:disable RSpec/VerifiedDoubles - non_project_remote_identity.origin_user_id, - project_remote_identity.origin_user_id, - admin_remote_identity.origin_user_id - ])) - end - - it "does not change the remote group" do - service_call - - expect(add_user_to_group_service).not_to have_received(:call) - expect(remove_user_from_group_service).not_to have_received(:call) - end - end - - context "when admin was explicitly added to the project with a restricted role" do - let(:project) { create(:project, members: { admin_user => role }) } - - it "grants the user admin-level permissions" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes( - file_id: "123", user_permissions: array_including( - { - user_id: admin_remote_identity.origin_user_id, - permissions: %i[read_files write_files create_files delete_files share_files] - } - ) - ) - ) - end - end - - context "when project folder was not yet created" do - let!(:project_storage) { create(:project_storage, :as_automatically_managed, project:, storage:, project_folder_id: "") } - - it "does not touch permissions" do - service_call - - expect(set_permissions_service).not_to have_received(:call) - end - end - - context "when there are multiple project storages" do - let!(:other_project_storage) { create(:project_storage, :as_automatically_managed, storage:, project_folder_id: "456") } - let!(:manual_project_storage) do - create(:project_storage, project_folder_mode: "manual", storage:, project_folder_id: "789") - end - - it "sets permissions for all active, automatically managed project storages" do - service_call - - expect(set_permissions_service).to have_received(:call).twice - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes(file_id: "123") - ) - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes(file_id: "456") - ) - end - - context "and when a project storage scope is passed" do - subject(:service_call) { described_class.call(storage:, project_storages_scope: project_storage_scope) } - - let(:project_storage_scope) { Storages::ProjectStorage.where(id: [project_storage.id, manual_project_storage.id]) } - - it "only works on project storages from the scope" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes(file_id: "123") - ) - end - - it "does not work on inactive or non-automatically managed project storages within the scope" do - service_call - - expect(set_permissions_service).not_to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes(file_id: "789") - ) - end - end - end - - context "when project storages exist for other storages" do - let!(:other_project_storage) { create(:project_storage, :as_automatically_managed, project_folder_id: "456") } - - it "only works on project storages for current storage" do - service_call - - expect(set_permissions_service).to have_received(:call).once - expect(set_permissions_service).to have_received(:call).with( - storage:, - auth_strategy:, - input_data: having_attributes(file_id: "123") - ) - end - end - - context "when fetching users of remote group fails" do - let(:group_users_service) { double(call: ServiceResult.failure(errors: Storages::StorageError.new(code: 418))) } # rubocop:disable RSpec/VerifiedDoubles - - it { is_expected.to be_failure } - - it "has errors" do - expect(service_call.errors).to be_present - end - - it "does not change users of remote group" do - service_call - - expect(add_user_to_group_service).not_to have_received(:call) - expect(remove_user_from_group_service).not_to have_received(:call) - end - - it "updates permissions" do - service_call - - expect(set_permissions_service).to have_received(:call) - end - end - - context "when adding users to remote group fails" do - let(:add_user_to_group_service) { double(call: ServiceResult.failure(errors: Storages::StorageError.new(code: 418))) } # rubocop:disable RSpec/VerifiedDoubles - - it { is_expected.to be_success } - - it "has errors" do - expect(service_call.errors).to be_present - end - - it "attempts all changes to remote group" do - service_call - - expect(add_user_to_group_service).to have_received(:call).at_least(:once) - expect(remove_user_from_group_service).to have_received(:call).at_least(:once) - end - - it "updates permissions" do - service_call - - expect(set_permissions_service).to have_received(:call) - end - end - - context "when removing users from remote group fails" do - let(:remove_user_from_group_service) { double(call: ServiceResult.failure(errors: Storages::StorageError.new(code: 418))) } # rubocop:disable RSpec/VerifiedDoubles - - it { is_expected.to be_success } - - it "has errors" do - expect(service_call.errors).to be_present - end - - it "attempts all changes to remote group" do - service_call - - expect(add_user_to_group_service).to have_received(:call).at_least(:once) - expect(remove_user_from_group_service).to have_received(:call).at_least(:once) - end - - it "updates permissions" do - service_call - - expect(set_permissions_service).to have_received(:call) - end - end - - context "when setting permissions fails" do - let(:set_permissions_service) { double(call: ServiceResult.failure(errors: Storages::StorageError.new(code: 418))) } # rubocop:disable RSpec/VerifiedDoubles - - it { is_expected.to be_success } - - it "has errors" do - expect(service_call.errors).to be_present - end - - it "applies all changes to remote group" do - service_call - - expect(add_user_to_group_service).to have_received(:call).at_least(:once) - expect(remove_user_from_group_service).to have_received(:call).at_least(:once) - end - - it "attempts to update permissions" do - service_call - - expect(set_permissions_service).to have_received(:call) - end + class TestIdentifier < Adapters::Providers::Nextcloud::ManagedFolderIdentifier + def initialize(project_storage) + super + @project = FakeProject.new(-273, project_storage.project.name) end end + + RSpec.describe NextcloudManagedFolderPermissionsService, :webmock do + shared_let(:oidc_provider) { create(:oidc_provider) } + + shared_let(:admin) { create(:admin) } + shared_let(:multiple_projects_user) { create(:user) } + shared_let(:single_project_user) { create(:user, authentication_provider: oidc_provider) } + shared_let(:oidc_admin) { create(:admin, authentication_provider: oidc_provider) } + shared_let(:storage) { create(:nextcloud_storage_with_local_connection, :as_automatically_managed) } + + shared_let(:remote_identities) do + [create(:remote_identity, + user: admin, + auth_source: storage.oauth_client, + integration: storage, + origin_user_id: "anakin"), + create(:remote_identity, + user: multiple_projects_user, + auth_source: storage.oauth_client, + integration: storage, + origin_user_id: "leia"), + create(:remote_identity, + user: single_project_user, + auth_source: oidc_provider, + integration: storage, + origin_user_id: "luke")] + end + + subject(:service) { described_class.new(storage:) } + + shared_let(:non_member_role) { create(:non_member, permissions: ["read_files"]) } + shared_let(:ordinary_role) { create(:project_role, permissions: %w[read_files write_files]) } + + shared_let(:project) do + create(:project, name: "[Sample] Project Name / Ehuu", + members: { multiple_projects_user => ordinary_role, single_project_user => ordinary_role }) + end + shared_let(:project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", storage:, project:) + end + + shared_let(:disallowed_chars_project) do + create(:project, name: '<=o=> | "Jedi" Project Folder ///', members: { multiple_projects_user => ordinary_role }) + end + shared_let(:disallowed_chars_project_storage) do + create(:project_storage, :with_historical_data, + project_folder_mode: "automatic", project: disallowed_chars_project, storage:) + end + + shared_let(:inactive_project) do + create(:project, name: "INACTIVE PROJECT! f0r r34lz", active: false, + members: { multiple_projects_user => ordinary_role }) + end + shared_let(:inactive_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: inactive_project, storage:) + end + + shared_let(:public_project) { create(:public_project, name: "PUBLIC PROJECT", active: true) } + shared_let(:public_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: public_project, storage:) + end + + before do + Adapters::Registry.stub("nextcloud.models.managed_folder_identifier", TestIdentifier) + setup_remote_folders + end + + after { delete_remote_folders } + + describe "#call" do + it "adds already logged in users to the project folder", vcr: "nextcloud/managed_folder_set_permissions" do + expect(service.call).to be_success + + # Group, user1, user2... + expect(remote_permissions_for(project_storage)).to contain_exactly( + { user_id: "OpenProject", permissions: [] }, + { user_id: "OpenProject", permissions: described_class::FILE_PERMISSIONS }, + { user_id: "anakin", permissions: described_class::FILE_PERMISSIONS }, + { user_id: "luke", permissions: %i[read_files write_files] }, + { user_id: "leia", permissions: %i[read_files write_files] } + ) + + expect(remote_permissions_for(disallowed_chars_project_storage)).to contain_exactly( + { user_id: "OpenProject", permissions: [] }, + { user_id: "OpenProject", + permissions: described_class::FILE_PERMISSIONS }, + { user_id: "anakin", + permissions: described_class::FILE_PERMISSIONS }, + { user_id: "leia", + permissions: %i[read_files write_files] } + ) + + expect(remote_permissions_for(inactive_project_storage)).to be_empty + end + + it "if the project is public allows any logged in user to read the files", + vcr: "nextcloud/managed_folder_set_permissions_public" do + service.call + + expect(remote_permissions_for(public_project_storage)).to contain_exactly( + { user_id: "OpenProject", permissions: [] }, + { user_id: "OpenProject", + permissions: described_class::FILE_PERMISSIONS }, + { user_id: "anakin", + permissions: described_class::FILE_PERMISSIONS }, + { user_id: "admin", permissions: [:read_files] }, + { user_id: "luke", permissions: [:read_files] }, + { user_id: "leia", permissions: [:read_files] } + ) + end + + it "ensures that admins have full access to all folders", vcr: "nextcloud/managed_folder_set_permissions_admin_access" do + service.call + + [project_storage, disallowed_chars_project_storage, public_project_storage].each do |ps| + expect(remote_permissions_for(ps)) + .to include({ user_id: "anakin", permissions: %i[read_files write_files create_files delete_files share_files] }) + end + end + + it "adds and remove users from the remote group", vcr: "nextcloud/managed_folder_set_permissions_group_users" do + service.call + + users = Adapters::Input::GroupUsers.build(group: storage.group).bind do |input_data| + Adapters::Registry["nextcloud.queries.group_users"].call(storage:, auth_strategy:, input_data:).value! + end + + expect(users).to match_array(%w[OpenProject anakin luke leia admin]) + ensure + %w[anakin luke leia].each do |user| + Adapters::Input::RemoveUserFromGroup.build(group: storage.group, user:).bind do |input_data| + Adapters::Registry["nextcloud.commands.remove_user_from_group"].call(storage:, auth_strategy:, input_data:) + end + end + end + end + + private + + def setup_remote_folders + storage.project_storages.each do |project_storage| + Adapters::Input::CreateFolder + .build(folder_name: project_storage.managed_project_folder_path, parent_location: "/").bind do |input_data| + Adapters::Registry["nextcloud.commands.create_folder"] + .call(storage:, auth_strategy:, input_data:) + .bind { project_storage.update(project_folder_id: it.id) } + end + end + end + + def delete_remote_folders + storage.project_storages.each do |project_storage| + Adapters::Input::DeleteFolder.build(location: project_storage.managed_project_folder_path).bind do |input_data| + Adapters::Registry["nextcloud.commands.delete_folder"].call(storage:, auth_strategy:, input_data:) + end + end + end + + def remote_permissions_for(project_storage) + Adapters::Authentication[auth_strategy].call(storage:) do |http| + request_url = UrlBuilder.url(storage.uri, "remote.php/dav/files", storage.username, + project_storage.managed_project_folder_path) + response = http.request(:propfind, request_url, xml: permission_request_body) + parse_acl_xml response.body.to_s + end + end + + def permission_request_body + Nokogiri::XML::Builder.new do |xml| + xml["d"].propfind( + "xmlns:d" => "DAV:", + "xmlns:nc" => "http://nextcloud.org/ns" + ) do + xml["d"].prop do + xml["nc"].send(:"acl-list") + end + end + end.to_xml + end + + def parse_acl_xml(xml) + found_code = "d:status[text() = 'HTTP/1.1 200 OK']" + not_found_code = "d:status[text() = 'HTTP/1.1 404 Not Found']" + happy_path = "/d:multistatus/d:response/d:propstat[#{found_code}]/d:prop/nc:acl-list" + not_found_path = "/d:multistatus/d:response/d:propstat[#{not_found_code}]/d:prop" + + if Nokogiri::XML(xml).xpath(not_found_path).children.map(&:name).include?("acl-list") + [] + else + Nokogiri::XML(xml).xpath(happy_path).children.map do |acl| + acl.children.each_with_object({ user_id: "", permissions: [] }) do |entry, agg| + agg[:user_id] = entry.text if entry.name == "acl-mapping-id" + agg[:permissions] = translate_mask_to_permissions(entry.text.to_i) if entry.name == "acl-permissions" + end + end + end + end + + def translate_mask_to_permissions(number) + Adapters::Providers::Nextcloud::Commands::SetPermissionsCommand::PERMISSIONS_MAP + .each_with_object([]) { |(permission, mask), list| list << permission if number & mask == mask } + end + + def set_permissions_on(file_id, user_permissions) + Adapters::Input::SetPermissions.build(user_permissions:, file_id:).bind do |input_data| + Adapters::Registry["nextcloud.commands.set_permissions"].call(storage:, auth_strategy:, input_data:) + end + end + + def create_folder_for(project_storage, folder_override = nil) + folder_name = folder_override || project_storage.managed_project_folder_name + Adapters::Input::CreateFolder.build(parent_location: storage.group_folder, folder_name:).bind do |input_data| + Adapters::Registry["nextcloud.commands.create_folder"].call(storage:, auth_strategy:, input_data:) + end + end + + def auth_strategy = Adapters::Registry["nextcloud.authentication.userless"].call + end end diff --git a/modules/storages/spec/services/storages/one_drive_managed_folder_create_service_spec.rb b/modules/storages/spec/services/storages/one_drive_managed_folder_create_service_spec.rb index 92a4aba2d63..aaaaaada200 100644 --- a/modules/storages/spec/services/storages/one_drive_managed_folder_create_service_spec.rb +++ b/modules/storages/spec/services/storages/one_drive_managed_folder_create_service_spec.rb @@ -33,344 +33,338 @@ require_module_spec_helper RSpec::Matchers.define_negated_matcher :not_change, :change -RSpec.describe Storages::OneDriveManagedFolderCreateService, :webmock do - shared_let(:admin) { create(:admin) } - shared_let(:storage) do - # Automatically Managed Project Folder Drive - create(:sharepoint_dev_drive_storage, - drive_id: "b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2ODRDvn3haLiQIhB5UYNdqMy", - oauth_client_token_user: admin) - end - shared_let(:admin_remote_identity) do - create(:remote_identity, - auth_source: storage.oauth_client, - user: admin, - integration: storage, - origin_user_id: "33db2c84-275d-46af-afb0-c26eb786b194") - end - - shared_let(:oidc_provider) { create(:oidc_provider) } - - # USER FACTORIES - shared_let(:oidc_user) { create(:user, authentication_provider: oidc_provider) } - shared_let(:single_project_user) { oidc_user } - shared_let(:single_project_user_remote_identity) do - create(:remote_identity, - user: single_project_user, - auth_source: oidc_user.authentication_provider, - integration: storage, - origin_user_id: "2ff33b8f-2843-40c1-9a17-d786bca17fba") - end - - shared_let(:multiple_projects_user) { create(:user) } - shared_let(:multiple_project_user_remote_identity) do - create(:remote_identity, - user: multiple_projects_user, - auth_source: storage.oauth_client, - integration: storage, - origin_user_id: "248aeb72-b231-4e71-a466-67fa7df2a285") - end - - # ROLE FACTORIES - shared_let(:ordinary_role) { create(:project_role, permissions: %w[read_files write_files]) } - shared_let(:read_only_role) { create(:project_role, permissions: %w[read_files]) } - shared_let(:non_member_role) { create(:non_member, permissions: %w[read_files]) } - - # PROJECT FACTORIES - shared_let(:project) do - create(:project, - name: "[Sample] Project Name / Ehuu", - members: { multiple_projects_user => ordinary_role, - oidc_user => ordinary_role, - single_project_user => ordinary_role }) - end - shared_let(:project_storage) do - create(:project_storage, :with_historical_data, project_folder_mode: "automatic", storage:, project:) - end - - shared_let(:disallowed_chars_project) do - create(:project, name: '<=o=> | "Jedi" Project Folder ///', members: { multiple_projects_user => ordinary_role }) - end - shared_let(:disallowed_chars_project_storage) do - create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: disallowed_chars_project, storage:) - end - - shared_let(:inactive_project) do - create(:project, name: "INACTIVE PROJECT! f0r r34lz", active: false, members: { multiple_projects_user => ordinary_role }) - end - shared_let(:inactive_project_storage) do - create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: inactive_project, storage:) - end - - shared_let(:public_project) { create(:public_project, name: "PUBLIC PROJECT", active: true) } - shared_let(:public_project_storage) do - create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: public_project, storage:) - end - - shared_let(:unmanaged_project) do - create(:project, name: "Non Managed Project", active: true, members: { multiple_projects_user => ordinary_role }) - end - shared_let(:unmanaged_project_storage) do - create(:project_storage, :with_historical_data, project_folder_mode: "manual", project: unmanaged_project, storage:) - end - - # This is a remote service call. We need to enable WebMock and VCR in order to record it, - # otherwise it will run the request every test suite run. - # Then we disable both VCR and WebMock to return to the usual state - shared_let(:original_folder_ids) do - use_storages_vcr_cassette("one_drive/sync_service_original_folders") do - original_folders(storage) +module Storages + RSpec.describe OneDriveManagedFolderCreateService, :webmock do + shared_let(:admin) { create(:admin) } + shared_let(:storage) do + # Automatically Managed Project Folder Drive + create(:sharepoint_dev_drive_storage, + drive_id: "b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2ODRDvn3haLiQIhB5UYNdqMy", + oauth_client_token_user: admin) + end + shared_let(:admin_remote_identity) do + create(:remote_identity, + auth_source: storage.oauth_client, + user: admin, + integration: storage, + origin_user_id: "33db2c84-275d-46af-afb0-c26eb786b194") end - end - subject(:service) { described_class.new(storage:) } + shared_let(:oidc_provider) { create(:oidc_provider) } - describe "#call" do - before { storage.update(automatically_managed: true) } - after { delete_created_folders } + # USER FACTORIES + shared_let(:oidc_user) { create(:user, authentication_provider: oidc_provider) } + shared_let(:single_project_user) { oidc_user } + shared_let(:single_project_user_remote_identity) do + create(:remote_identity, + user: single_project_user, + auth_source: oidc_user.authentication_provider, + integration: storage, + origin_user_id: "2ff33b8f-2843-40c1-9a17-d786bca17fba") + end - describe "Remote Folder Creation", vcr: "one_drive/sync_service_create_folder" do - let(:single_project_user_origin_user_id) { single_project_user_remote_identity.origin_user_id } - let(:multiple_project_user_origin_user_id) { multiple_project_user_remote_identity.origin_user_id } - let(:admin_origin_user_id) { admin_remote_identity.origin_user_id } + shared_let(:multiple_projects_user) { create(:user) } + shared_let(:multiple_project_user_remote_identity) do + create(:remote_identity, + user: multiple_projects_user, + auth_source: storage.oauth_client, + integration: storage, + origin_user_id: "248aeb72-b231-4e71-a466-67fa7df2a285") + end - it "updates the project folder id for all active automatically managed projects" do - expect { service.call }.to change { disallowed_chars_project_storage.reload.project_folder_id } - .from(nil).to(String) - .and(change { project_storage.reload.project_folder_id }.from(nil).to(String)) - .and(change { public_project_storage.reload.project_folder_id }.from(nil).to(String)) - .and(not_change { inactive_project_storage.reload.project_folder_id }) - .and(not_change { unmanaged_project_storage.reload.project_folder_id }) + # ROLE FACTORIES + shared_let(:ordinary_role) { create(:project_role, permissions: %w[read_files write_files]) } + shared_let(:read_only_role) { create(:project_role, permissions: %w[read_files]) } + shared_let(:non_member_role) { create(:non_member, permissions: %w[read_files]) } + + # PROJECT FACTORIES + shared_let(:project) do + create(:project, + name: "[Sample] Project Name / Ehuu", + members: { multiple_projects_user => ordinary_role, + oidc_user => ordinary_role, + single_project_user => ordinary_role }) + end + shared_let(:project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", storage:, project:) + end + + shared_let(:disallowed_chars_project) do + create(:project, name: '<=o=> | "Jedi" Project Folder ///', members: { multiple_projects_user => ordinary_role }) + end + shared_let(:disallowed_chars_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: disallowed_chars_project, + storage:) + end + + shared_let(:inactive_project) do + create(:project, name: "INACTIVE PROJECT! f0r r34lz", active: false, members: { multiple_projects_user => ordinary_role }) + end + shared_let(:inactive_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: inactive_project, storage:) + end + + shared_let(:public_project) { create(:public_project, name: "PUBLIC PROJECT", active: true) } + shared_let(:public_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: public_project, storage:) + end + + shared_let(:unmanaged_project) do + create(:project, name: "Non Managed Project", active: true, members: { multiple_projects_user => ordinary_role }) + end + shared_let(:unmanaged_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "manual", project: unmanaged_project, storage:) + end + + # This is a remote service call. We need to enable WebMock and VCR in order to record it, + # otherwise it will run the request every test suite run. + # Then we disable both VCR and WebMock to return to the usual state + shared_let(:original_folder_ids) do + use_storages_vcr_cassette("one_drive/sync_service_original_folders") do + original_folders end + end - it "adds a record to the LastProjectFolder for each new folder" do - scope = ->(project_storage) { Storages::LastProjectFolder.where(project_storage:).last } + subject(:service) { described_class.new(storage:) } - expect { service.call }.to not_change { scope[unmanaged_project_storage].reload.origin_folder_id } - .and(not_change { scope[inactive_project_storage].reload.origin_folder_id }) + describe "#call" do + before { storage.update(automatically_managed: true) } + after { delete_created_folders } - expect(scope[project_storage].origin_folder_id).to eq(project_storage.reload.project_folder_id) - expect(scope[public_project_storage].origin_folder_id).to eq(public_project_storage.reload.project_folder_id) - expect(scope[disallowed_chars_project_storage].origin_folder_id) - .to eq(disallowed_chars_project_storage.reload.project_folder_id) - end + describe "Remote Folder Creation", vcr: "one_drive/sync_service_create_folder" do + let(:single_project_user_origin_user_id) { single_project_user_remote_identity.origin_user_id } + let(:multiple_project_user_origin_user_id) { multiple_project_user_remote_identity.origin_user_id } + let(:admin_origin_user_id) { admin_remote_identity.origin_user_id } - it "creates the remote folders for all projects with automatically managed folders enabled" do - service.call + it "updates the project folder id for all active automatically managed projects" do + expect { service.call }.to change { disallowed_chars_project_storage.reload.project_folder_id } + .from(nil).to(String) + .and(change { project_storage.reload.project_folder_id }.from(nil).to(String)) + .and(change { public_project_storage.reload.project_folder_id }.from(nil).to(String)) + .and(not_change { inactive_project_storage.reload.project_folder_id }) + .and(not_change { unmanaged_project_storage.reload.project_folder_id }) + end - [project_storage, disallowed_chars_project_storage, public_project_storage].each do |proj_storage| - expect(project_folder_info(proj_storage)).to be_success + it "adds a record to the LastProjectFolder for each new folder" do + scope = ->(project_storage) { LastProjectFolder.where(project_storage:).last } + + expect { service.call }.to not_change { scope[unmanaged_project_storage].reload.origin_folder_id } + .and(not_change { scope[inactive_project_storage].reload.origin_folder_id }) + + expect(scope[project_storage].origin_folder_id).to eq(project_storage.reload.project_folder_id) + expect(scope[public_project_storage].origin_folder_id).to eq(public_project_storage.reload.project_folder_id) + expect(scope[disallowed_chars_project_storage].origin_folder_id) + .to eq(disallowed_chars_project_storage.reload.project_folder_id) + end + + it "creates the remote folders for all projects with automatically managed folders enabled" do + service.call + + [project_storage, disallowed_chars_project_storage, public_project_storage].each do |proj_storage| + expect(project_folder_info(proj_storage)).to be_success + end + end + + it "makes sure that the last_project_folder.origin_folder_id match the current project_folder_id" do + service.call + + [project_storage, disallowed_chars_project_storage, public_project_storage].each do |proj_storage| + proj_storage.reload + the_real_last_project_folder = proj_storage.last_project_folders.last + + expect(proj_storage.project_folder_id).to eq(the_real_last_project_folder.origin_folder_id) + end end end - it "makes sure that the last_project_folder.origin_folder_id match the current project_folder_id" do - service.call + it "renames an already existing project folder", vcr: "one_drive/sync_service_rename_folder" do + original_folder = create_folder_for(disallowed_chars_project_storage, "Old Jedi Project") + disallowed_chars_project_storage.update(project_folder_id: original_folder.id) - [project_storage, disallowed_chars_project_storage, public_project_storage].each do |proj_storage| - proj_storage.reload - the_real_last_project_folder = proj_storage.last_project_folders.last + service_result = service.call + expect(service_result).to be_success + expect(service_result.errors).to be_empty - expect(proj_storage.project_folder_id).to eq(the_real_last_project_folder.origin_folder_id) - end + result = project_folder_info(disallowed_chars_project_storage).value! + expect(result.name).to match(/_=o=_ _ _Jedi_ Project Folder ___ \(\d+\)/) end - end - it "renames an already existing project folder", vcr: "one_drive/sync_service_rename_folder" do - original_folder = create_folder_for(disallowed_chars_project_storage, "Old Jedi Project") + it "hides (removes all permissions) from inactive project folders", vcr: "one_drive/sync_service_hide_inactive" do + original_folder = create_folder_for(inactive_project_storage) + inactive_project_storage.update(project_folder_id: original_folder.id) - disallowed_chars_project_storage.update(project_folder_id: original_folder.result.id) + set_permissions_on(original_folder.id, + [{ user_id: "2ff33b8f-2843-40c1-9a17-d786bca17fba", permissions: [:read_files] }, + { user_id: "248aeb72-b231-4e71-a466-67fa7df2a285", permissions: [:write_files] }, + { user_id: "33db2c84-275d-46af-afb0-c26eb786b194", permissions: [:write_files] }]) - service_result = service.call - expect(service_result).to be_success - expect(service_result.errors).to be_empty + expect(remote_permissions_for(inactive_project_storage)) + .to eq({ read: ["2ff33b8f-2843-40c1-9a17-d786bca17fba"], + write: %w[248aeb72-b231-4e71-a466-67fa7df2a285 33db2c84-275d-46af-afb0-c26eb786b194] }) - result = project_folder_info(disallowed_chars_project_storage).result - expect(result.name).to match(/_=o=_ _ _Jedi_ Project Folder ___ \(\d+\)/) - end + result = service.call - it "hides (removes all permissions) from inactive project folders", vcr: "one_drive/sync_service_hide_inactive" do - original_folder = create_folder_for(inactive_project_storage) - inactive_project_storage.update(project_folder_id: original_folder.result.id) + expect(result).to be_success + expect(result.errors).to be_empty + expect(remote_permissions_for(inactive_project_storage)).to be_empty + end - set_permissions_on(original_folder.result.id, - [{ user_id: "2ff33b8f-2843-40c1-9a17-d786bca17fba", permissions: [:read_files] }, - { user_id: "248aeb72-b231-4e71-a466-67fa7df2a285", permissions: [:write_files] }, - { user_id: "33db2c84-275d-46af-afb0-c26eb786b194", permissions: [:write_files] }]) + describe "error handling" do + let(:error_key_prefix) { "services.errors.models.one_drive_sync_service" } - expect(permissions_for(inactive_project_storage)) - .to eq({ read: ["2ff33b8f-2843-40c1-9a17-d786bca17fba"], - write: %w[248aeb72-b231-4e71-a466-67fa7df2a285 33db2c84-275d-46af-afb0-c26eb786b194] }) + before { allow(Rails.logger).to receive_messages(%i[error warn]) } - result = service.call + context "when reading the root folder fails" do + before { storage.update(drive_id: "THIS-IS-NOT-A-DRIVE-ID") } - expect(result).to be_success - expect(result.errors).to be_empty - expect(permissions_for(inactive_project_storage)).to be_empty - end + it "returns a failure in case retrieving the root list fails", vcr: "one_drive/sync_service_root_read_failure" do + result = service.call - describe "error handling" do - let(:error_key_prefix) { "services.errors.models.one_drive_sync_service" } + expect(result).to be_failure + expect(result.errors[:remote_folders]) + .to match_array(I18n.t("#{error_key_prefix}.attributes.remote_folders.request_error", drive_id: storage.drive_id)) + end - before { allow(Rails.logger).to receive_messages(%i[error warn]) } - - context "when reading the root folder fails" do - before { storage.update(drive_id: "THIS-IS-NOT-A-DRIVE-ID") } - - it "returns a failure in case retrieving the root list fails", vcr: "one_drive/sync_service_root_read_failure" do - result = service.call - - expect(result).to be_failure - expect(result.errors[:remote_folders]) - .to match_array(I18n.t("#{error_key_prefix}.attributes.remote_folders.request_error", drive_id: storage.drive_id)) + it "logs the occurrence", vcr: "one_drive/sync_service_root_read_failure" do + service.call + expect(Rails.logger) + .to have_received(:error) + .with(error_code: :request_error, drive_id: storage.drive_id, data: { body: /drive id/, status: Integer }) + end end - it "logs the occurrence", vcr: "one_drive/sync_service_root_read_failure" do + it "does not break in case of timeout", vcr: "one_drive/sync_service_timeout" do + skip "The timeout setting isn't working as expected" + stub_request_with_timeout(:get, /\/root\/children$/) service.call expect(Rails.logger) .to have_received(:error) - .with(error_code: :request_error, drive_id: storage.drive_id, message: nil, data: /drive id/) - end - end - - it "does not break in case of timeout", vcr: "one_drive/sync_service_timeout" do - skip "The timeout setting isn't working as expected" - stub_request_with_timeout(:get, /\/root\/children$/) - service.call - - expect(Rails.logger) - .to have_received(:error) - .with(command: described_class, - message: nil, - data: { body: /timed out while waiting on select/, status: nil }) - end - - context "when folder creation fails" do - it "doesn't update the project_storage", vcr: "one_drive/sync_service_creation_fail" do - already_existing_folder = create_folder_for(project_storage).result - result = nil - - expect { result = service.call }.not_to change(project_storage, :project_folder_id) - - expect(result).to be_success - expect(result.errors[:create_folder]) - .to match_array(I18n.t("#{error_key_prefix}.attributes.create_folder.conflict", - folder_name: project_storage.managed_project_folder_path, parent_location: "/")) - ensure - delete_folder(already_existing_folder.id) - end - - it "logs the occurrence", vcr: "one_drive/sync_service_creation_fail" do - already_existing_folder = create_folder_for(project_storage).result - service.call - - expect(Rails.logger) - .to have_received(:error) - .with(folder_name: "[Sample] Project Name _ Ehuu (#{project.id})", - error_code: :conflict, + .with(command: described_class, message: nil, - data: /nameAlreadyExists/) - ensure - delete_folder(already_existing_folder.id) + data: { body: /timed out while waiting on select/, status: nil }) end - end - context "when folder renaming fails" do - it "adds an error and logs the occurrence", vcr: "one_drive/sync_service_rename_failed" do - already_existing_folder = create_folder_for(project_storage) - original_folder = create_folder_for(project_storage, "Flawless Death Star Blueprints") - project_storage.update(project_folder_id: original_folder.result.id) + context "when folder creation fails" do + it "doesn't update the project_storage", vcr: "one_drive/sync_service_creation_fail" do + already_existing_folder = create_folder_for(project_storage) + result = nil - result = service.call + expect { result = service.call }.not_to change(project_storage, :project_folder_id) - expect(result.errors[:rename_project_folder]) - .to match_array(I18n.t("#{error_key_prefix}.attributes.rename_project_folder.conflict", - current_path: original_folder.result.name, - project_folder_name: project_storage.managed_project_folder_path)) + expect(result).to be_failure + expect(result.errors[:create_folder]) + .to match_array(I18n.t("#{error_key_prefix}.attributes.create_folder.conflict", + folder_name: project_storage.managed_project_folder_path, parent_location: "/")) + ensure + delete_folder(already_existing_folder.id) + end - expect(Rails.logger) - .to have_received(:error).with(folder_id: project_storage.project_folder_id, - folder_name: "[Sample] Project Name _ Ehuu (#{project.id})", - error_code: :conflict, - message: nil, - data: /nameAlreadyExists/) - ensure - delete_folder(already_existing_folder.result.id) + it "logs the occurrence", vcr: "one_drive/sync_service_creation_fail" do + already_existing_folder = create_folder_for(project_storage) + service.call + + expect(Rails.logger) + .to have_received(:error) + .with(folder_name: "[Sample] Project Name _ Ehuu (#{project.id})", + parent_location: "/", + error_code: :conflict, + data: { body: /nameAlreadyExists/, status: Integer }) + ensure + delete_folder(already_existing_folder.id) + end + end + + context "when folder renaming fails" do + it "adds an error and logs the occurrence", vcr: "one_drive/sync_service_rename_failed" do + already_existing_folder = create_folder_for(project_storage) + original_folder = create_folder_for(project_storage, "Flawless Death Star Blueprints") + project_storage.update(project_folder_id: original_folder.id) + + result = service.call + + expect(result.errors[:rename_project_folder]) + .to match_array(I18n.t("#{error_key_prefix}.attributes.rename_project_folder.conflict", + current_path: original_folder.name, + project_folder_name: project_storage.managed_project_folder_path)) + + expect(Rails.logger) + .to have_received(:error).with(current_path: original_folder.name, + project_folder_id: project_storage.project_folder_id, + project_folder_name: "[Sample] Project Name _ Ehuu (#{project.id})", + error_code: :conflict, + data: { body: /nameAlreadyExists/, status: Integer }) + ensure + delete_folder(already_existing_folder.id) + end end end end - end - private + private - def permissions_for(project_storage) - return if project_folder_info(project_storage).failure? + def remote_permissions_for(project_storage) + return if project_folder_info(project_storage).failure? - Storages::Peripherals::StorageInteraction::Authentication[auth_strategy].call(storage:) do |http| - response = http.get(Storages::UrlBuilder.url(storage.uri, - "/v1.0/drives", - storage.drive_id, - "/items", - project_storage.project_folder_id, - "/permissions")) - response.json(symbolize_keys: true).fetch(:value, []).each_with_object({}) do |grant, hash| - next if grant[:roles].member?("owner") + Adapters::Authentication[auth_strategy].call(storage:) do |http| + response = http.get(UrlBuilder.url(storage.uri, + "/v1.0/drives", + storage.drive_id, + "/items", + project_storage.project_folder_id, + "/permissions")) + response.json(symbolize_keys: true).fetch(:value, []).each_with_object({}) do |grant, hash| + next if grant[:roles].member?("owner") - hash[grant[:roles].first.to_sym] ||= [] - hash[grant[:roles].first.to_sym] << grant.dig(:grantedToV2, :user, :id) + hash[grant[:roles].first.to_sym] ||= [] + hash[grant[:roles].first.to_sym] << grant.dig(:grantedToV2, :user, :id) + end end end - end - def original_folders(_storage) - root_folder_contents - .on_success { |result| return result.result.files.select(&:folder?).map(&:id) } - end - - def project_folder_info(project_storage) - root_folder_contents.map do |storage_files| - storage_files.files.find { |file| file.id == project_storage.project_folder_id } + def original_folders + root_folder_contents.bind { return it.all_folders.map(&:id) } end - end - def root_folder_contents - Storages::Peripherals::Registry.resolve("one_drive.queries.files") - .call(storage:, auth_strategy:, folder: Storages::Peripherals::ParentFolder.new("/")) - end + def project_folder_info(project_storage) + root_folder_contents.fmap do |storage_files| + storage_files.files.find { |file| file.id == project_storage.project_folder_id } + end + end - def create_folder_for(project_storage, folder_override = nil) - folder_name = folder_override || project_storage.managed_project_folder_path - parent_location = Storages::Peripherals::ParentFolder.new("/") + def root_folder_contents + Adapters::Input::Files.build(folder: "/").bind do |input_data| + Adapters::Registry.resolve("one_drive.queries.files").call(storage:, auth_strategy:, input_data:) + end + end - Storages::Peripherals::Registry.resolve("one_drive.commands.create_folder") - .call(storage: project_storage.storage, - auth_strategy:, - folder_name:, - parent_location:) - end + def create_folder_for(project_storage, folder_override = nil) + folder_name = folder_override || project_storage.managed_project_folder_path - def set_permissions_on(file_id, user_permissions) - input_data = Storages::Peripherals::StorageInteraction::Inputs::SetPermissions - .build(file_id:, user_permissions:) - .value! - Storages::Peripherals::Registry.resolve("one_drive.commands.set_permissions") - .call(storage:, auth_strategy:, input_data:) - .on_failure { p it.inspect } - end + Adapters::Input::CreateFolder.build(folder_name:, parent_location: "/").bind do |input_data| + Adapters::Registry.resolve("one_drive.commands.create_folder") + .call(storage: project_storage.storage, auth_strategy:, input_data:) + .value_or { fail "Folder creation failed" } + end + end - def delete_created_folders - storage.project_storages.automatic - .where(storage:) - .with_project_folder - .find_each { |project_storage| delete_folder(project_storage.project_folder_id) } - end + def set_permissions_on(file_id, user_permissions) + Adapters::Input::SetPermissions.build(file_id:, user_permissions:).bind do |input_data| + Adapters::Registry.resolve("one_drive.commands.set_permissions").call(storage:, auth_strategy:, input_data:) + end + end - def delete_folder(item_id) - Storages::Peripherals::Registry.resolve("one_drive.commands.delete_folder") - .call(storage:, auth_strategy:, location: item_id) - end + def delete_created_folders + storage.project_storages.automatic.where(storage:) + .with_project_folder.find_each { |project_storage| delete_folder(project_storage.project_folder_id) } + end - def auth_strategy - Storages::Peripherals::Registry.resolve("one_drive.authentication.userless").call + def delete_folder(item_id) + Adapters::Input::DeleteFolder.build(location: item_id).bind do |input_data| + Adapters::Registry.resolve("one_drive.commands.delete_folder").call(storage:, auth_strategy:, input_data:) + end + end + + def auth_strategy = Adapters::Registry.resolve("one_drive.authentication.userless").call end end diff --git a/modules/storages/spec/services/storages/one_drive_managed_folder_permissions_service_spec.rb b/modules/storages/spec/services/storages/one_drive_managed_folder_permissions_service_spec.rb index a82b48c7b5b..0bf323fb969 100644 --- a/modules/storages/spec/services/storages/one_drive_managed_folder_permissions_service_spec.rb +++ b/modules/storages/spec/services/storages/one_drive_managed_folder_permissions_service_spec.rb @@ -33,385 +33,383 @@ require_module_spec_helper RSpec::Matchers.define_negated_matcher :not_change, :change -RSpec.describe Storages::OneDriveManagedFolderPermissionsService, :webmock do - shared_let(:admin) { create(:admin) } - shared_let(:storage) do - # Automatically Managed Project Folder Drive - create(:sharepoint_dev_drive_storage, - drive_id: "b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2ODRDvn3haLiQIhB5UYNdqMy", - oauth_client_token_user: admin) - end - shared_let(:admin_remote_identity) do - create(:remote_identity, - auth_source: storage.oauth_client, - user: admin, - integration: storage, - origin_user_id: "33db2c84-275d-46af-afb0-c26eb786b194") - end - - shared_let(:oidc_provider) { create(:oidc_provider) } - - # USER FACTORIES - shared_let(:oidc_user) do - identity_url = "#{oidc_provider.slug}:qweqweqweqwe" - create(:user, identity_url:) - end - shared_let(:single_project_user) { oidc_user } - shared_let(:single_project_user_remote_identity) do - create(:remote_identity, - user: single_project_user, - auth_source: oidc_user.authentication_provider, - integration: storage, - origin_user_id: "2ff33b8f-2843-40c1-9a17-d786bca17fba") - end - - shared_let(:multiple_projects_user) { create(:user) } - shared_let(:multiple_project_user_remote_identity) do - create(:remote_identity, - user: multiple_projects_user, - auth_source: storage.oauth_client, - integration: storage, - origin_user_id: "248aeb72-b231-4e71-a466-67fa7df2a285") - end - - # ROLE FACTORIES - shared_let(:ordinary_role) { create(:project_role, permissions: %w[read_files write_files]) } - shared_let(:read_only_role) { create(:project_role, permissions: %w[read_files]) } - shared_let(:non_member_role) { create(:non_member, permissions: %w[read_files]) } - - # PROJECT FACTORIES - shared_let(:project) do - create(:project, - name: "[Sample] Project Name / Ehuu", - members: { multiple_projects_user => ordinary_role, - oidc_user => ordinary_role, - single_project_user => ordinary_role }) - end - shared_let(:project_storage) do - create :project_storage, :with_historical_data, project_folder_mode: "automatic", - storage:, - project:, - project_folder_id: "01AZJL5PKF6CYXWCIXVNDIF6RXTCRH5OOK" - end - - shared_let(:disallowed_chars_project) do - create(:project, name: '<=o=> | "Jedi" Project Folder ///', members: { multiple_projects_user => ordinary_role }) - end - shared_let(:disallowed_chars_project_storage) do - create :project_storage, :with_historical_data, project_folder_mode: "automatic", - project: disallowed_chars_project, - storage:, - project_folder_id: "01AZJL5PKVY6USXYVCNJFINFV32VEZRP4K" - end - - shared_let(:inactive_project) do - create(:project, name: "INACTIVE PROJECT! f0r r34lz", active: false, members: { multiple_projects_user => ordinary_role }) - end - shared_let(:inactive_project_storage) do - create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: inactive_project, storage:) - end - - shared_let(:public_project) { create(:public_project, name: "PUBLIC PROJECT", active: true) } - shared_let(:public_project_storage) do - create :project_storage, :with_historical_data, project_folder_mode: "automatic", - project: public_project, - storage:, - project_folder_id: "01AZJL5PI473R5DL4W4BB3SLISDSVFFDXZ" - end - - shared_let(:unmanaged_project) do - create(:project, name: "Non Managed Project", active: true, members: { multiple_projects_user => ordinary_role }) - end - shared_let(:unmanaged_project_storage) do - create :project_storage, :with_historical_data, project_folder_mode: "manual", - project: unmanaged_project, - storage:, - project_folder_id: "SHOULD-NOT-BE-REQUESTED" - end - - # This is a remote service call. We need to enable WebMock and VCR in order to record it, - # otherwise it will run the request every test suite run. - # Then we disable both VCR and WebMock to return to the usual state - shared_let(:original_folder_ids) do - use_storages_vcr_cassette("one_drive/sync_service_original_folders") do - original_folders(storage) +module Storages + RSpec.describe OneDriveManagedFolderPermissionsService, :webmock do + shared_let(:admin) { create(:admin) } + shared_let(:storage) do + # Automatically Managed Project Folder Drive + create(:sharepoint_dev_drive_storage, + drive_id: "b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2ODRDvn3haLiQIhB5UYNdqMy", + oauth_client_token_user: admin) + end + shared_let(:admin_remote_identity) do + create(:remote_identity, + auth_source: storage.oauth_client, + user: admin, + integration: storage, + origin_user_id: "33db2c84-275d-46af-afb0-c26eb786b194") end - end - subject(:service) { described_class.new(storage:) } + shared_let(:oidc_provider) { create(:oidc_provider) } - describe "#call" do - before { storage.update(automatically_managed: true) } - after { delete_created_folders } + # USER FACTORIES + shared_let(:oidc_user) do + identity_url = "#{oidc_provider.slug}:qweqweqweqwe" + create(:user, identity_url:) + end + shared_let(:single_project_user) { oidc_user } + shared_let(:single_project_user_remote_identity) do + create(:remote_identity, + user: single_project_user, + auth_source: oidc_user.authentication_provider, + integration: storage, + origin_user_id: "2ff33b8f-2843-40c1-9a17-d786bca17fba") + end - context "when folder were newly created", vcr: "one_drive/sync_service_create_folder" do - let(:set_permissions) { Storages::Peripherals::StorageInteraction::OneDrive::SetPermissionsCommand } - let(:single_project_user_origin_user_id) { single_project_user_remote_identity.origin_user_id } - let(:multiple_project_user_origin_user_id) { multiple_project_user_remote_identity.origin_user_id } - let(:admin_origin_user_id) { admin_remote_identity.origin_user_id } + shared_let(:multiple_projects_user) { create(:user) } + shared_let(:multiple_project_user_remote_identity) do + create(:remote_identity, + user: multiple_projects_user, + auth_source: storage.oauth_client, + integration: storage, + origin_user_id: "248aeb72-b231-4e71-a466-67fa7df2a285") + end - before { allow(set_permissions).to receive(:call).and_call_original } + # ROLE FACTORIES + shared_let(:ordinary_role) { create(:project_role, permissions: %w[read_files write_files]) } + shared_let(:read_only_role) { create(:project_role, permissions: %w[read_files]) } + shared_let(:non_member_role) { create(:non_member, permissions: %w[read_files]) } - it "sets permissions for folders exactly 3 times" do - service.call + # PROJECT FACTORIES + shared_let(:project) do + create(:project, + name: "[Sample] Project Name / Ehuu", + members: { multiple_projects_user => ordinary_role, + oidc_user => ordinary_role, + single_project_user => ordinary_role }) + end + shared_let(:project_storage) do + create :project_storage, :with_historical_data, project_folder_mode: "automatic", + storage:, + project:, + project_folder_id: "01AZJL5PKF6CYXWCIXVNDIF6RXTCRH5OOK" + end - expect(set_permissions).to have_received(:call).with( - auth_strategy: an_instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - input_data: an_instance_of(Storages::Peripherals::StorageInteraction::Inputs::SetPermissions), - storage: an_instance_of(Storages::OneDriveStorage) - ).exactly(3).times + shared_let(:disallowed_chars_project) do + create(:project, name: '<=o=> | "Jedi" Project Folder ///', members: { multiple_projects_user => ordinary_role }) + end + shared_let(:disallowed_chars_project_storage) do + create :project_storage, :with_historical_data, project_folder_mode: "automatic", + project: disallowed_chars_project, + storage:, + project_folder_id: "01AZJL5PKVY6USXYVCNJFINFV32VEZRP4K" + end + + shared_let(:inactive_project) do + create(:project, name: "INACTIVE PROJECT! f0r r34lz", active: false, members: { multiple_projects_user => ordinary_role }) + end + shared_let(:inactive_project_storage) do + create(:project_storage, :with_historical_data, project_folder_mode: "automatic", project: inactive_project, storage:) + end + + shared_let(:public_project) { create(:public_project, name: "PUBLIC PROJECT", active: true) } + shared_let(:public_project_storage) do + create :project_storage, :with_historical_data, project_folder_mode: "automatic", + project: public_project, + storage:, + project_folder_id: "01AZJL5PI473R5DL4W4BB3SLISDSVFFDXZ" + end + + shared_let(:unmanaged_project) do + create(:project, name: "Non Managed Project", active: true, members: { multiple_projects_user => ordinary_role }) + end + shared_let(:unmanaged_project_storage) do + create :project_storage, :with_historical_data, project_folder_mode: "manual", + project: unmanaged_project, + storage:, + project_folder_id: "SHOULD-NOT-BE-REQUESTED" + end + + # This is a remote service call. We need to enable WebMock and VCR in order to record it, + # otherwise it will run the request every test suite run. + # Then we disable both VCR and WebMock to return to the usual state + shared_let(:original_folder_ids) do + use_storages_vcr_cassette("one_drive/sync_service_original_folders") do + original_folders(storage) end + end - it "sets permissions for project's (private with 3 members) folder according to member's roles" do - service.call + subject(:service) { described_class.new(storage:) } - expect(set_permissions).to have_received(:call).with( - auth_strategy: an_instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - input_data: having_attributes( - file_id: project_storage.project_folder_id, - user_permissions: - [{ user_id: admin_origin_user_id, permissions: [:write_files] }, - { user_id: single_project_user_origin_user_id, permissions: [:write_files] }, - { user_id: multiple_project_user_origin_user_id, permissions: [:write_files] }] - ), - storage: an_instance_of(Storages::OneDriveStorage) - ).once - end + describe "#call" do + before { storage.update(automatically_managed: true) } + after { delete_created_folders } - it "sets permissions for project's (private with 1 members) folder according to member's roles" do - service.call + context "when folder were newly created", vcr: "one_drive/sync_service_create_folder" do + let(:set_permissions) { Adapters::Providers::OneDrive::Commands::SetPermissionsCommand } + let(:single_project_user_origin_user_id) { single_project_user_remote_identity.origin_user_id } + let(:multiple_project_user_origin_user_id) { multiple_project_user_remote_identity.origin_user_id } + let(:admin_origin_user_id) { admin_remote_identity.origin_user_id } - expect(set_permissions).to have_received(:call).with( - auth_strategy: an_instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - input_data: having_attributes( - file_id: disallowed_chars_project_storage.project_folder_id, - user_permissions: - [ - # admin(not a member of the project) receives write access as expected - { user_id: admin_origin_user_id, permissions: [:write_files] }, - { user_id: multiple_project_user_origin_user_id, permissions: [:write_files] } - ] - ), - storage: an_instance_of(Storages::OneDriveStorage) - ).once - end + before { allow(set_permissions).to receive(:call).and_call_original } - it "sets permissions for project's (public with 0 members) folder appropriately" do - service.call - - expect(set_permissions).to have_received(:call).with( - auth_strategy: an_instance_of(Storages::Peripherals::StorageInteraction::AuthenticationStrategies::Strategy), - input_data: having_attributes( - file_id: public_project_storage.project_folder_id, - user_permissions: - [ - # admin gets write access - { user_id: admin_origin_user_id, permissions: [:write_files] }, - # other non members get read access - { user_id: single_project_user_origin_user_id, permissions: [:read_files] }, - { user_id: multiple_project_user_origin_user_id, permissions: [:read_files] } - ] - ), - storage: an_instance_of(Storages::OneDriveStorage) - ).once - end - - context "when passing a project storages scope" do - subject(:service) { described_class.new(storage:, project_storages_scope:) } - - let(:project_storages_scope) { Storages::ProjectStorage.where(id: [project_storage.id, unmanaged_project_storage.id]) } - - it "sets permissions for the active project storage in scope" do + it "sets permissions for folders exactly 3 times" do service.call - expect(set_permissions).to have_received(:call).once expect(set_permissions).to have_received(:call).with( - auth_strategy: anything, - storage: anything, - input_data: having_attributes(file_id: project_storage.project_folder_id) + auth_strategy:, + input_data: an_instance_of(Adapters::Input::SetPermissions), + storage: an_instance_of(OneDriveStorage) + ).exactly(3).times + end + + it "sets permissions for project's (private with 3 members) folder according to member's roles" do + service.call + + expect(set_permissions).to have_received(:call).with( + auth_strategy:, + input_data: having_attributes( + file_id: project_storage.project_folder_id, + user_permissions: + [{ user_id: admin_origin_user_id, permissions: [:write_files] }, + { user_id: single_project_user_origin_user_id, permissions: [:write_files] }, + { user_id: multiple_project_user_origin_user_id, permissions: [:write_files] }] + ), + storage: an_instance_of(OneDriveStorage) ).once end - it "ignores the unmanaged project storage in scope" do + it "sets permissions for project's (private with 1 members) folder according to member's roles" do service.call - expect(set_permissions).not_to have_received(:call).with( - auth_strategy: anything, - storage: anything, - input_data: having_attributes(file_id: unmanaged_project_storage.project_folder_id) - ) + expect(set_permissions).to have_received(:call).with( + auth_strategy:, + input_data: having_attributes( + file_id: disallowed_chars_project_storage.project_folder_id, + user_permissions: + [ + # admin(not a member of the project) receives write access as expected + { user_id: admin_origin_user_id, permissions: [:write_files] }, + { user_id: multiple_project_user_origin_user_id, permissions: [:write_files] } + ] + ), + storage: an_instance_of(OneDriveStorage) + ).once end - it "ignores a managed project storage outside the scope" do + it "sets permissions for project's (public with 0 members) folder appropriately" do service.call - expect(set_permissions).not_to have_received(:call).with( - auth_strategy: anything, - storage: anything, - input_data: having_attributes(file_id: public_project_storage.project_folder_id) - ) + expect(set_permissions).to have_received(:call).with( + auth_strategy:, + input_data: having_attributes( + file_id: public_project_storage.project_folder_id, + user_permissions: + [ + # admin gets write access + { user_id: admin_origin_user_id, permissions: [:write_files] }, + # other non members get read access + { user_id: single_project_user_origin_user_id, permissions: [:read_files] }, + { user_id: multiple_project_user_origin_user_id, permissions: [:read_files] } + ] + ), + storage: an_instance_of(OneDriveStorage) + ).once + end + + context "when passing a project storages scope" do + subject(:service) { described_class.new(storage:, project_storages_scope:) } + + let(:project_storages_scope) { ProjectStorage.where(id: [project_storage.id, unmanaged_project_storage.id]) } + + it "sets permissions for the active project storage in scope" do + service.call + + expect(set_permissions).to have_received(:call).once + expect(set_permissions).to have_received(:call).with( + auth_strategy: anything, + storage: anything, + input_data: having_attributes(file_id: project_storage.project_folder_id) + ).once + end + + it "ignores the unmanaged project storage in scope" do + service.call + + expect(set_permissions).not_to have_received(:call).with( + auth_strategy: anything, + storage: anything, + input_data: having_attributes(file_id: unmanaged_project_storage.project_folder_id) + ) + end + + it "ignores a managed project storage outside the scope" do + service.call + + expect(set_permissions).not_to have_received(:call).with( + auth_strategy: anything, + storage: anything, + input_data: having_attributes(file_id: public_project_storage.project_folder_id) + ) + end end end - end - context "when users are already logged in", vcr: "one_drive/sync_service_set_permissions" do - before do - # ensuring the project_folder_ids match the cassette - project_storage.update!(project_folder_id: "01AZJL5PLIGSIHNQX7VVHJQHXH6WGXTKZQ") - disallowed_chars_project_storage.update!(project_folder_id: "01AZJL5PMLKINPNTC5JZFLF2RNI5QODOOK") - public_project_storage.update!(project_folder_id: "01AZJL5PK3YKOMDXIHRRDLAXFU5BJ4KLXY") - inactive_project_storage.update!(project_folder_id: "01AZJL5PK24YOPXISOHVF3A56DI2EATQC5") - end - - it "adds them to the project folder" do - original_folder = create_folder_for(inactive_project_storage) - inactive_project_storage.update(project_folder_id: original_folder.result.id) - - service.call - - expect(permissions_for(project_storage)) - .to eq({ write: %w[248aeb72-b231-4e71-a466-67fa7df2a285 - 2ff33b8f-2843-40c1-9a17-d786bca17fba - 33db2c84-275d-46af-afb0-c26eb786b194] }) - - expect(permissions_for(disallowed_chars_project_storage)) - .to include({ write: %w[248aeb72-b231-4e71-a466-67fa7df2a285 33db2c84-275d-46af-afb0-c26eb786b194] }) - - expect(permissions_for(inactive_project_storage)).to be_empty - end - end - - context "when the project is public", vcr: "one_drive/sync_service_public_project" do - before do - # ensuring the project_folder_ids match the cassette - project_storage.update!(project_folder_id: "01AZJL5PLBQEL7TBIV5FD2HOAR4LSCH3HF") - disallowed_chars_project_storage.update!(project_folder_id: "01AZJL5PLTTFRO3FI2SNCK7AL6JV6CPSB6") - public_project_storage.update!(project_folder_id: "01AZJL5PIISB6WZDU6AVCLDNEGCY22UULI") - end - - it "allows any logged in user to read the files" do - service.call - - expect(permissions_for(public_project_storage)) - .to eq({ read: %w[248aeb72-b231-4e71-a466-67fa7df2a285 2ff33b8f-2843-40c1-9a17-d786bca17fba], - write: ["33db2c84-275d-46af-afb0-c26eb786b194"] }) - end - end - - context "when the user is an admin", vcr: "one_drive/sync_service_admin_access" do - before do - # ensuring the project_folder_ids match the cassette - project_storage.update!(project_folder_id: "01AZJL5PN33BSGWNSWKRHYXH74YI4QLSDH") - disallowed_chars_project_storage.update!(project_folder_id: "01AZJL5PIXSSWKBU73FFGKZN6LXAULGDXO") - public_project_storage.update!(project_folder_id: "01AZJL5PMDPQHEYW65ENB26FQNWK73Y7NU") - end - - it "ensures they have full access to all folders" do - service.call - - [project_storage, disallowed_chars_project_storage, public_project_storage].each do |ps| - expect(permissions_for(ps)[:write]).to include("33db2c84-275d-46af-afb0-c26eb786b194") - end - end - end - - describe "error handling" do - let(:error_key_prefix) { "services.errors.models.one_drive_sync_service" } - - before { allow(Rails.logger).to receive_messages(%i[error warn]) } - - context "when setting permission fails" do + context "when users are already logged in", vcr: "one_drive/sync_service_set_permissions" do before do # ensuring the project_folder_ids match the cassette - project_storage.update!(project_folder_id: "01AZJL5POLGVTUAI3545DJ6CN24YVYIGMV") - disallowed_chars_project_storage.update!(project_folder_id: "01AZJL5PNQGJLKUIKERBFKYNTB732KYF3V") - public_project_storage.update!(project_folder_id: "01AZJL5PIYDDYS33Z4T5E2GODLZ2ABLOFV") + project_storage.update!(project_folder_id: "01AZJL5PLIGSIHNQX7VVHJQHXH6WGXTKZQ") + disallowed_chars_project_storage.update!(project_folder_id: "01AZJL5PMLKINPNTC5JZFLF2RNI5QODOOK") + public_project_storage.update!(project_folder_id: "01AZJL5PK3YKOMDXIHRRDLAXFU5BJ4KLXY") + inactive_project_storage.update!(project_folder_id: "01AZJL5PK24YOPXISOHVF3A56DI2EATQC5") end - it "logs the occurrence", vcr: "one_drive/sync_service_fail_add_user" do - single_project_user_remote_identity.update(origin_user_id: "my_name_is_mud") + it "adds them to the project folder" do + original_folder = create_folder_for(inactive_project_storage) + inactive_project_storage.update(project_folder_id: original_folder.id) service.call - expect(Rails.logger) - .to have_received(:error) - .with(error_code: :bad_request, - message: nil, - data: /noResolvedUsers/).twice + + expect(remote_permissions_for(project_storage)) + .to eq({ write: %w[248aeb72-b231-4e71-a466-67fa7df2a285 + 2ff33b8f-2843-40c1-9a17-d786bca17fba + 33db2c84-275d-46af-afb0-c26eb786b194] }) + + expect(remote_permissions_for(disallowed_chars_project_storage)) + .to include({ write: %w[248aeb72-b231-4e71-a466-67fa7df2a285 33db2c84-275d-46af-afb0-c26eb786b194] }) + + expect(remote_permissions_for(inactive_project_storage)).to be_empty + end + end + + context "when the project is public", vcr: "one_drive/sync_service_public_project" do + before do + # ensuring the project_folder_ids match the cassette + project_storage.update!(project_folder_id: "01AZJL5PLBQEL7TBIV5FD2HOAR4LSCH3HF") + disallowed_chars_project_storage.update!(project_folder_id: "01AZJL5PLTTFRO3FI2SNCK7AL6JV6CPSB6") + public_project_storage.update!(project_folder_id: "01AZJL5PIISB6WZDU6AVCLDNEGCY22UULI") + end + + it "allows any logged in user to read the files" do + service.call + + expect(remote_permissions_for(public_project_storage)) + .to eq({ read: %w[248aeb72-b231-4e71-a466-67fa7df2a285 2ff33b8f-2843-40c1-9a17-d786bca17fba], + write: ["33db2c84-275d-46af-afb0-c26eb786b194"] }) + end + end + + context "when the user is an admin", vcr: "one_drive/sync_service_admin_access" do + before do + # ensuring the project_folder_ids match the cassette + project_storage.update!(project_folder_id: "01AZJL5PN33BSGWNSWKRHYXH74YI4QLSDH") + disallowed_chars_project_storage.update!(project_folder_id: "01AZJL5PIXSSWKBU73FFGKZN6LXAULGDXO") + public_project_storage.update!(project_folder_id: "01AZJL5PMDPQHEYW65ENB26FQNWK73Y7NU") + end + + it "ensures they have full access to all folders" do + service.call + + [project_storage, disallowed_chars_project_storage, public_project_storage].each do |ps| + expect(remote_permissions_for(ps)[:write]).to include("33db2c84-275d-46af-afb0-c26eb786b194") + end + end + end + + describe "error handling" do + let(:error_key_prefix) { "services.errors.models.one_drive_sync_service" } + + before { allow(Rails.logger).to receive_messages(%i[error warn]) } + + context "when setting permission fails" do + before do + # ensuring the project_folder_ids match the cassette + project_storage.update!(project_folder_id: "01AZJL5POLGVTUAI3545DJ6CN24YVYIGMV") + disallowed_chars_project_storage.update!(project_folder_id: "01AZJL5PNQGJLKUIKERBFKYNTB732KYF3V") + public_project_storage.update!(project_folder_id: "01AZJL5PIYDDYS33Z4T5E2GODLZ2ABLOFV") + end + + it "logs the occurrence", vcr: "one_drive/sync_service_fail_add_user" do + single_project_user_remote_identity.update(origin_user_id: "my_name_is_mud") + + service.call + expect(Rails.logger) + .to have_received(:error) + .with(error_code: :bad_request, + data: { body: /noResolvedUsers/, status: Integer }).twice + end end end end - end - private + private - def permissions_for(project_storage) - return if project_folder_info(project_storage).failure? + def remote_permissions_for(project_storage) + return if project_folder_info(project_storage).failure? - Storages::Peripherals::StorageInteraction::Authentication[auth_strategy].call(storage:) do |http| - response = http.get(Storages::UrlBuilder.url(storage.uri, - "/v1.0/drives", - storage.drive_id, - "/items", - project_storage.project_folder_id, - "/permissions")) - response.json(symbolize_keys: true).fetch(:value, []).each_with_object({}) do |grant, hash| - next if grant[:roles].member?("owner") + Adapters::Authentication[auth_strategy].call(storage:) do |http| + response = http.get(UrlBuilder.url(storage.uri, + "/v1.0/drives", + storage.drive_id, + "/items", + project_storage.project_folder_id, + "/permissions")) + response.json(symbolize_keys: true).fetch(:value, []).each_with_object({}) do |grant, hash| + next if grant[:roles].member?("owner") - hash[grant[:roles].first.to_sym] ||= [] - hash[grant[:roles].first.to_sym] << grant.dig(:grantedToV2, :user, :id) + hash[grant[:roles].first.to_sym] ||= [] + hash[grant[:roles].first.to_sym] << grant.dig(:grantedToV2, :user, :id) + end end end - end - def original_folders(_storage) - root_folder_contents - .on_success { |result| return result.result.files.select(&:folder?).map(&:id) } - end - - def project_folder_info(project_storage) - root_folder_contents.map do |storage_files| - storage_files.files.find { |file| file.id == project_storage.project_folder_id } + def original_folders(_storage) + root_folder_contents.fmap { it.all_folders.map(&:id) } end - end - def root_folder_contents - Storages::Peripherals::Registry.resolve("one_drive.queries.files") - .call(storage:, auth_strategy:, folder: Storages::Peripherals::ParentFolder.new("/")) - end + def project_folder_info(project_storage) + root_folder_contents.fmap do |storage_files| + storage_files.files.find { |file| file.id == project_storage.project_folder_id } + end + end - def create_folder_for(project_storage, folder_override = nil) - folder_name = folder_override || project_storage.managed_project_folder_path - parent_location = Storages::Peripherals::ParentFolder.new("/") + def root_folder_contents + Adapters::Input::Files.build(folder: "/").bind do |input_data| + Adapters::Registry.resolve("one_drive.queries.files").call(storage:, auth_strategy:, input_data:) + end + end - Storages::Peripherals::Registry.resolve("one_drive.commands.create_folder") - .call(storage: project_storage.storage, - auth_strategy:, - folder_name:, - parent_location:) - end + def create_folder_for(project_storage, folder_override = nil) + folder_name = folder_override || project_storage.managed_project_folder_path - def set_permissions_on(file_id, user_permissions) - input_data = Storages::Peripherals::StorageInteraction::Inputs::SetPermissions - .build(file_id:, user_permissions:) - .value! - Storages::Peripherals::Registry.resolve("one_drive.commands.set_permissions") - .call(storage:, auth_strategy:, input_data:) - .on_failure { p it.inspect } - end + Adapters::Input::CreateFolder.build(folder_name:, parent_location: "/").bind do |input_data| + Adapters::Registry.resolve("one_drive.commands.create_folder") + .call(storage: project_storage.storage, auth_strategy:, input_data:) + .value_or { fail it.inspect } + end + end - def delete_created_folders - storage.project_storages.automatic - .where(storage:) - .with_project_folder - .find_each { |project_storage| delete_folder(project_storage.project_folder_id) } - end + def set_permissions_on(file_id, user_permissions) + Adapters::Input::SetPermissions.build(file_id:, user_permissions:).bind do |input_data| + Adapters::Registry.resolve("one_drive.commands.set_permissions") + .call(storage:, auth_strategy:, input_data:) + .value_or { fail it.inspect } + end + end - def delete_folder(item_id) - Storages::Peripherals::Registry.resolve("one_drive.commands.delete_folder") - .call(storage:, auth_strategy:, location: item_id) - end + def delete_created_folders + storage.project_storages.automatic + .where(storage:) + .with_project_folder + .find_each { |project_storage| delete_folder(project_storage.project_folder_id) } + end - def auth_strategy - Storages::Peripherals::Registry.resolve("one_drive.authentication.userless").call + def delete_folder(item_id) + Adapters::Input::DeleteFolder.build(location: item_id).bind do |input_data| + Adapters::Registry.resolve("one_drive.commands.delete_folder").call(storage:, auth_strategy:, input_data:) + end + end + + def auth_strategy = Adapters::Registry.resolve("one_drive.authentication.userless").call end end diff --git a/modules/storages/spec/services/storages/project_storages/copy_project_folders_service_spec.rb b/modules/storages/spec/services/storages/project_storages/copy_project_folders_service_spec.rb index 882cc639001..d964b6c9eac 100644 --- a/modules/storages/spec/services/storages/project_storages/copy_project_folders_service_spec.rb +++ b/modules/storages/spec/services/storages/project_storages/copy_project_folders_service_spec.rb @@ -32,34 +32,33 @@ require "spec_helper" require_module_spec_helper RSpec.describe Storages::ProjectStorages::CopyProjectFoldersService, :webmock do - using Storages::Peripherals::ServiceResultRefinements + # using Storages::Peripherals::ServiceResultRefinements let(:storage) { create(:nextcloud_storage, :as_automatically_managed) } let(:target) { create(:project_storage, storage:) } let(:system_user) { create(:system) } - let(:result_data) { Storages::Peripherals::StorageInteraction::ResultData::CopyTemplateFolder.new(nil, nil, false) } + let(:result_data) { Storages::Adapters::Results::CopyTemplateFolder.new(nil, nil, false) } + let(:copy_folder_command) { class_double(Storages::Adapters::Providers::Nextcloud::Commands::CopyTemplateFolderCommand) } + let(:input_data) do + Storages::Adapters::Input::CopyTemplateFolder + .build(source: source.managed_project_folder_path, destination: target.managed_project_folder_path).value! + end + let(:auth_strategy) { Storages::Adapters::Registry["nextcloud.authentication.userless"].call } subject(:service) { described_class } + before { Storages::Adapters::Registry.stub("nextcloud.commands.copy_template_folder", copy_folder_command) } + context "with automatically managed project folders" do let(:source) { create(:project_storage, :as_automatically_managed, storage:) } + before do + allow(copy_folder_command).to receive(:call) + .with(storage:, auth_strategy:, input_data:) + .and_return(Success(result_data.with(polling_url: "https://polling.url.de/cool/subresources"))) + end + it "if polling is required, returns a nil id and an url" do - Storages::Peripherals::Registry - .stub("#{source.storage.short_provider_type}.commands.copy_template_folder", - ->(auth_strategy:, storage:, source_path:, destination_path:) do - strategy = Storages::Peripherals::Registry - .resolve("#{source.storage.short_provider_type}.authentication.userless").call - - expect(auth_strategy.class).to eq(strategy.class) - expect(storage).to eq(source.storage) - expect(source_path).to eq(source.project_folder_location) - expect(destination_path).to eq(target.managed_project_folder_path) - - # Return a success for the provider copy with no polling required - ServiceResult.success(result: result_data.with(polling_url: "https://polling.url.de/cool/subresources")) - end) - result = service.call(source:, target:) expect(result).to be_success @@ -108,10 +107,8 @@ RSpec.describe Storages::ProjectStorages::CopyProjectFoldersService, :webmock do let(:source) { create(:project_storage, :as_automatically_managed, storage:) } it "the target folder already exists" do - Storages::Peripherals::Registry - .stub("#{source.storage.short_provider_type}.commands.copy_template_folder", - ->(_) { build_failure(:conflict) }) - + allow(copy_folder_command).to receive(:call).with(storage:, auth_strategy:, input_data:) + .and_return(build_failure(:conflict)) result = service.call(source:, target:) expect(result).to be_failure @@ -121,9 +118,8 @@ RSpec.describe Storages::ProjectStorages::CopyProjectFoldersService, :webmock do end it "source folder was not found" do - Storages::Peripherals::Registry - .stub("#{source.storage.short_provider_type}.commands.copy_template_folder", - ->(_) { build_failure(:not_found) }) + allow(copy_folder_command).to receive(:call).with(storage:, auth_strategy:, input_data:) + .and_return(build_failure(:not_found)) result = service.call(source:, target:) @@ -134,10 +130,8 @@ RSpec.describe Storages::ProjectStorages::CopyProjectFoldersService, :webmock do end it "token is unauthorized to do the copy" do - Storages::Peripherals::Registry - .stub("#{source.storage.short_provider_type}.commands.copy_template_folder", - ->(_) { build_failure(:unauthorized) }) - + allow(copy_folder_command).to receive(:call).with(storage:, auth_strategy:, input_data:) + .and_return(build_failure(:unauthorized)) result = service.call(source:, target:) expect(result).to be_failure @@ -146,10 +140,8 @@ RSpec.describe Storages::ProjectStorages::CopyProjectFoldersService, :webmock do end it "token has no access to the source folder" do - Storages::Peripherals::Registry - .stub("#{source.storage.short_provider_type}.commands.copy_template_folder", - ->(_) { build_failure(:forbidden) }) - + allow(copy_folder_command).to receive(:call).with(storage:, auth_strategy:, input_data:) + .and_return(build_failure(:forbidden)) result = service.call(source:, target:) expect(result).to be_failure @@ -162,9 +154,7 @@ RSpec.describe Storages::ProjectStorages::CopyProjectFoldersService, :webmock do private def build_failure(code) - response = "Response info" - storage_error = Storages::Peripherals::StorageInteraction::OneDrive::Util - .storage_error(response:, code:, source: described_class, log_message: "Log message for #{code}") - ServiceResult.failure(result: code, errors: storage_error) + error = Storages::Adapters::Results::Error.new(source: copy_folder_command).with(code:) + Failure(error) end end diff --git a/modules/storages/spec/services/storages/project_storages/delete_service_spec.rb b/modules/storages/spec/services/storages/project_storages/delete_service_spec.rb index 478abae6cdc..33138ac0233 100644 --- a/modules/storages/spec/services/storages/project_storages/delete_service_spec.rb +++ b/modules/storages/spec/services/storages/project_storages/delete_service_spec.rb @@ -35,10 +35,10 @@ require_relative "shared_event_gun_examples" RSpec.describe Storages::ProjectStorages::DeleteService, :webmock, type: :model do shared_examples_for "deleting project storages with project folders" do - let(:command_double) { class_double(command_class_reference, call: ServiceResult.success) } + let(:command_double) { class_double(command_class_reference, call: Success()) } before do - Storages::Peripherals::Registry + Storages::Adapters::Registry .stub("#{storage}.commands.delete_folder", command_double) end @@ -53,7 +53,8 @@ RSpec.describe Storages::ProjectStorages::DeleteService, :webmock, type: :model end context "if project folder deletion request fails" do - let(:command_double) { class_double(command_class_reference, call: ServiceResult.failure(result: 404)) } + let(:error) { Storages::Adapters::Results::Error.new(code: :conflict, source: command_class_reference) } + let(:command_double) { class_double(command_class_reference, call: Failure(error)) } it "tries to remove the project folder at the remote storage and still succeed with deletion" do expect(described_class.new(model: project_storage, user:).call).to be_success @@ -131,10 +132,8 @@ RSpec.describe Storages::ProjectStorages::DeleteService, :webmock, type: :model end before do - Storages::Peripherals::Registry.stub( - "#{model_instance.storage.short_provider_type}.commands.delete_folder", - ->(*) { ServiceResult.success } - ) + Storages::Adapters::Registry.stub("#{model_instance.storage}.commands.delete_folder", + ->(*) { Success() }) end it_behaves_like("an event gun", OpenProject::Events::PROJECT_STORAGE_DESTROYED) @@ -143,10 +142,6 @@ RSpec.describe Storages::ProjectStorages::DeleteService, :webmock, type: :model private def command_class_reference - if storage.provider_type_nextcloud? - Storages::Peripherals::StorageInteraction::Nextcloud::DeleteFolderCommand - else - Storages::Peripherals::StorageInteraction::OneDrive::DeleteFolderCommand - end + Storages::Adapters::Registry["#{project_storage.storage}.commands.delete_folder"] end end diff --git a/modules/storages/spec/services/storages/storages/update_service_spec.rb b/modules/storages/spec/services/storages/storages/update_service_spec.rb index 3640583b062..7fdf1b86dcb 100644 --- a/modules/storages/spec/services/storages/storages/update_service_spec.rb +++ b/modules/storages/spec/services/storages/storages/update_service_spec.rb @@ -71,8 +71,7 @@ RSpec.describe Storages::Storages::UpdateService, type: :model do it "cannot update storage creator" do storage_creator = create(:admin, login: "storage_creator") storage = create(:nextcloud_storage, creator: storage_creator) - service = described_class.new(user: create(:admin), - model: storage) + service = described_class.new(user: create(:admin), model: storage) service_result = service.call(creator: create(:user, login: "impostor")) diff --git a/modules/storages/spec/spec_helper.rb b/modules/storages/spec/spec_helper.rb index 123a9bffb04..6a4a861903f 100644 --- a/modules/storages/spec/spec_helper.rb +++ b/modules/storages/spec/spec_helper.rb @@ -62,8 +62,14 @@ end Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each { |f| require f } RSpec.configure do |config| - config.prepend_before { Storages::Peripherals::Registry.enable_stubs! } - config.append_after { Storages::Peripherals::Registry.unstub } + config.include Dry::Monads[:result] + + config.prepend_before do + Storages::Adapters::Registry.enable_stubs! + end + config.append_after do + Storages::Adapters::Registry.unstub + end config.define_derived_metadata(file_path: %r{/modules/storages/spec}) do |metadata| metadata[:vcr_cassette_library_dir] = STORAGES_CASSETTE_LIBRARY_DIR diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service.yml new file mode 100644 index 00000000000..5228d153cb0 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service.yml @@ -0,0 +1,1504 @@ +--- +http_interactions: +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '192' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:12 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=718ddc43a80c427b947f4cf6e4838216; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=M%2F4TwA5JuRfQ2aDLlMc%2FEKxmEgdRN%2BGLJ5H4dzOqgIiXbDPMNI8jynJc57RRxo5r%2BCeSXJRhw9%2Bwo5pVrkJL4p9%2BZu5%2Bi7jHCoZp%2BI6eaDiKtVsZ0h1hcT2Xozkhw53%2F; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=718ddc43a80c427b947f4cf6e4838216; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=718ddc43a80c427b947f4cf6e4838216; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=718ddc43a80c427b947f4cf6e4838216; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=cc45041215f8ce4d313fb90b7a74f825; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - lNOYXk0ZHRE1ccSBRxMg + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - lNOYXk0ZHRE1ccSBRxMg + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '402' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/103groupOpenProjectOpenProject311userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1)/180groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/%5bdev%5d%20Empty%20(3)/181groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8)/204groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:12 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/103 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:12 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=08b4334240baa8c0b0b6e615ebef87c0; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=iVzHUKZfSzzEYuyf0IeOVY3i22AYMEUxKITxDOShXmEg0RoUf2HAS3xmoRVT1kS%2FrhGdVP6UPnP3vVHBuJc5S6lsW8T0oA6gbpk3ZmQOBtgwz8AsoSaH6%2FzmryCZQWxk; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=08b4334240baa8c0b0b6e615ebef87c0; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=08b4334240baa8c0b0b6e615ebef87c0; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=08b4334240baa8c0b0b6e615ebef87c0; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fc6f766bd45a31b25fe636edf7e3049f; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - DLMmDNBZifaVgkbB79AZ + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '251' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":103,"name":"OpenProject","mtime":1746203592,"ctime":0,"mimetype":"application\/x-op-directory","size":90335,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:12 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 1 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:12 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=0bcb9d0b18af354c88619dd3b975389b; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=vluhD5vWa1XtpB0qOPXTcxOC6XdDm%2FONnLGY0lhG6GzAbV2QERD05d8RpxZ3nI46XsQSVOdpVf%2B5vXHGmdUVTn7wNAkvy%2B8qsBh9%2FMWy6uuN466jrrbcq01YGG9HlCcg; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0bcb9d0b18af354c88619dd3b975389b; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=0bcb9d0b18af354c88619dd3b975389b; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=0bcb9d0b18af354c88619dd3b975389b; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=b0f43b9312ba74de8721c42afc6ff251; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - wrhUp4ZOHpKsmCZkYuFf + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - wrhUp4ZOHpKsmCZkYuFf + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '350' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Oc-Fileid: + - '00000835ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=bf82b8f4c4619daf1aa25c95168f5af8; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=nNyeWZ5dZUVbwSXfrghghag5RGisWjatutORf%2BConsBT0cw%2Bc4K47i6REl6FGIK024enAgMWN5fF3nC3Kzf%2BRUc6CxWV3pZgrDhWQc3prh6h%2F3Ndf4PBWA%2FRSqAktWMs; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=bf82b8f4c4619daf1aa25c95168f5af8; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=bf82b8f4c4619daf1aa25c95168f5af8; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=bf82b8f4c4619daf1aa25c95168f5af8; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=735e0cd8ac8dd2481ffe08d6037d6e87; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - mIHuDXOEBtCax1KwJMdr + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - mIHuDXOEBtCax1KwJMdr + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1de2f2f6859029421f6a3437a1f2ed80; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=8WmqyKRddrwvup7gNrg8bdYb%2BasexC1L1RwaVbJHqMWiSQ%2Fjf4Pvp2pxU9cZ680I5MNknMcdYhRkthK7kWqeY2HFPz75J7S1w%2BGPFTTMPVQ8qQNT7kBQHYrIUyNVoNSz; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1de2f2f6859029421f6a3437a1f2ed80; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1de2f2f6859029421f6a3437a1f2ed80; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=1de2f2f6859029421f6a3437a1f2ed80; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=eeff0350a52ce89d419ff4efa951a378; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - njWrRJTUNurr7S5FA5aP + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - njWrRJTUNurr7S5FA5aP + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '360' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/8350Fri, 02 May 2025 16:33:13 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Oc-Fileid: + - '00000836ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=ca0fc05b52541adc355ad75a0709efab; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=ZZ7h1Vs4tD53DTBSQYdmMGBgAcHhoCPV%2F7wxrcCJLHFq8eHyVuhdSUuQiI%2B%2FdFz8YAmtppCC2BgUz%2FWTNyTWfC9x6R%2FjoVwAm1EEtsDZsZ8I7LK%2FTbTXwnSiddX9A7%2B5; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ca0fc05b52541adc355ad75a0709efab; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=ca0fc05b52541adc355ad75a0709efab; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=ca0fc05b52541adc355ad75a0709efab; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9e421f2a6ce856946e21ffec18a1e1ad; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - EqYXQXC4i79U5etqTnyY + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - EqYXQXC4i79U5etqTnyY + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=e008c0e8de5633e521ea2873a776d1e2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=nRnMSPN4YDubWfMziwc5LOXFjJ7cZ%2FFTSl4WYePg%2BC1G%2B6vwsnL46BRDbOPT%2FLadSIVMtIbEdpi4gYTsmE%2Fnl9BWR0%2FGDMsmpPsm3xIm18TDFfhG6Gco1RGFV3EqXEiZ; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e008c0e8de5633e521ea2873a776d1e2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=e008c0e8de5633e521ea2873a776d1e2; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=e008c0e8de5633e521ea2873a776d1e2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=094645d2eb490170901558b37bcff8f5; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - jdPbEsCcne3xqwtBmnsl + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - jdPbEsCcne3xqwtBmnsl + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '367' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/8360Fri, 02 May 2025 16:33:13 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Oc-Fileid: + - '00000837ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d6dd1c50d0123407acf4cd0f59189aad; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=CurnjoLdpvnfUkv3QBfoGojkTQALH2ro2mPiPQt%2BQ8Hl0Mx5OAMikSRSU%2BvxFZGznbKV0lCFffxKfbE9cN53eLNn%2FFLgnc07RELGPaUmjzRKx6v0aC080GSyeECasN%2FY; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d6dd1c50d0123407acf4cd0f59189aad; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d6dd1c50d0123407acf4cd0f59189aad; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d6dd1c50d0123407acf4cd0f59189aad; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=5b1fc3dbf0bc042582dbb673a49015c4; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - zCB8lifKfQs0qlE4owuS + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - zCB8lifKfQs0qlE4owuS + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=ff254be05628efddd3e1e4a7294fa5f2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=2Yfo7x%2BJvllS6fAxZ6xqvrE5Ax1shPoiVfBdH%2F8XBKRSsf%2F4HSbkIbb%2FavbcPj5%2FSggBGYAJR%2BKUg%2BLaZLWrCgKbhwk6WqvcM3LLLLmhjiY3Zlx4dRB9FjwHsU827SeT; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ff254be05628efddd3e1e4a7294fa5f2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=ff254be05628efddd3e1e4a7294fa5f2; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=ff254be05628efddd3e1e4a7294fa5f2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2785d4eea9071dbf7ad101b1155aea10; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - hCtHRsLcnPEtK35wS5Of + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - hCtHRsLcnPEtK35wS5Of + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '345' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/8370Fri, 02 May 2025 16:33:13 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/180 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=895c585b5bb2ca1799f52eda5fc61e51; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=HB6bcHLY43Bd1aLmP8cxOl6oFKm0F1HVKxL8XjK9weCuTacVup%2FI7zBDlMPAFpyhrd6uyOlIRmEeG0sN18eps%2FHzLZqLkUhzAtf2oJp0PdUI5mdE5jb3YSSV5d7%2BHbgm; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=895c585b5bb2ca1799f52eda5fc61e51; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=895c585b5bb2ca1799f52eda5fc61e51; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=895c585b5bb2ca1799f52eda5fc61e51; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d230d52f4e428e59a01c575d08d15cec; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 33Y82r5Wl37NB9Qf3o8V + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '267' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":180,"name":"Demo + project (1)","mtime":1736432335,"ctime":0,"mimetype":"application\/x-op-directory","size":924,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/Demo + project (1)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=30d9ce4d6039d3233524c84d17a399ad; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=FcXL02X0987y0mxz%2BZt8r0VVFnBtb07JToXpbFgqQJT5Jm6LxqtBbjUmdOuy88JCIrSdniSwB4i49jzH%2BYMaHYnEQ%2BZSSr2XRfPkGfAEm%2BQyNSmQHxmQQffN7fEj8dX0; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=30d9ce4d6039d3233524c84d17a399ad; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=30d9ce4d6039d3233524c84d17a399ad; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=30d9ce4d6039d3233524c84d17a399ad; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f8aee773ca92ca6fd970fc616badb57b; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - JcugU4oKNOYgp2WJSi27 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - JcugU4oKNOYgp2WJSi27 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '371' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/181 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=f9ec0fea7f976ef09878f210e5bfc5c1; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=bLmkLgQ0u3pFIUviwKzDuTkpkqC2tJ%2BPvCIshzp8wf1rwJUPJbexAvJtanhbAIFRYKMFDZX1JHq5TIlBBYZOpgAVDymvGAsW8pUxGUOkPJdXnVLudRqaoM3McaSQiqpy; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f9ec0fea7f976ef09878f210e5bfc5c1; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=f9ec0fea7f976ef09878f210e5bfc5c1; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=f9ec0fea7f976ef09878f210e5bfc5c1; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f3d70f60bed119b5bbcf33b9279c8c26; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - rhZlH7qiTwgq3sbGdbFo + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '267' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":181,"name":"[dev] + Empty (3)","mtime":1709925816,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/[dev] + Empty (3)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5Bdev%5D%20Empty%20(3) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=529071e4c00db3077e1176bec89492bd; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=CM2B7dHioE56ZCOqvA%2FFG4xAjzzXWbsipTaALi8Qlbofzt%2FGCUr3j3BfikXYDHMeUjTQvWGg5F%2Bs0I3W9lJeLgoiLP%2BV4LVSi46TCxWu7pqLB50MABK7PTqH%2F8loIHzV; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=529071e4c00db3077e1176bec89492bd; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=529071e4c00db3077e1176bec89492bd; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=529071e4c00db3077e1176bec89492bd; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=580938327605a8114d2f45362d888de3; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - lDiKXNGaDzcM3gNMaox1 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - lDiKXNGaDzcM3gNMaox1 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '374' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bdev%5d%20Empty%20(3)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:13 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/204 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:13 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=f149706f2b9133dbaed3d76ed6a9b8b5; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=0b7%2BOfeYF11hVGDR3ptJaMMoFQoj9Ix%2BcmR4moBdCPkodKJ1xzT7N50ix2vmIKkG40UrrIXGzZMPD%2BZcHQCaKXpDQxvqYEnhe0P6crMlb5g8L%2FAT2ckjosEF6o48yYoK; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f149706f2b9133dbaed3d76ed6a9b8b5; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=f149706f2b9133dbaed3d76ed6a9b8b5; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=f149706f2b9133dbaed3d76ed6a9b8b5; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=39c5de6e9399e61f3f6773525a60db0c; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 6qywVZUL85L4zPFqbRRy + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '266' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":204,"name":"Projectify + (8)","mtime":1736255538,"ctime":0,"mimetype":"application\/x-op-directory","size":89411,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/Projectify + (8)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:14 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:14 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=8123beff0d991bb925405724e4d7d875; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=LcI0FPfvseNxDhBiQ5dMmXCwPzzMtefPugt2Rud7BDHZ3KHktojLK4ueZoh73O4Wj84J9158jx%2BG%2BB2iWTqXyvhKd5wLotMGbu1D%2BhF1dhhbY4hBvRSTOlbvapz0kgVM; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8123beff0d991bb925405724e4d7d875; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=8123beff0d991bb925405724e4d7d875; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=8123beff0d991bb925405724e4d7d875; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=930a52eff3f9f13772516ff5cf1a86b1; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - Seugq2yHy3WKfgGGbq25 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - Seugq2yHy3WKfgGGbq25 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '367' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:14 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:14 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9be965fd494a273a6b18f0a5a9990633; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Ur%2FcP8lUW9AlBpigqdAXRpJbwddNg0HNxFZnPK17JRfWu%2FUSOijmyuShdmJIjBhy%2FO1%2Bk%2B7jYD9ZpjS9EdYgYhNGa5cDJeETc22FnawIMXXCP%2FeFb2yVC%2FZE2tIq%2Fijp; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9be965fd494a273a6b18f0a5a9990633; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9be965fd494a273a6b18f0a5a9990633; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=9be965fd494a273a6b18f0a5a9990633; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9dce3e684750aa41e7257a50c1710bf0; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - cP1jQvmBdTAV1PKYf3bz + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - cP1jQvmBdTAV1PKYf3bz + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:14 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:14 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=66cf89d2450b97444423768c9f7588c2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=OEZgF5U6MGRKT8lWvUgJRnGU8cdxa9Kf1%2BIfrf%2B2OqicLSarwikgBGK%2BrVmnlEx6YPn%2BizD5tG59RJkhjz1vjObhh1FNVJUTrnQtBShzkIqzfAWnmAV9VeRBcuA8VRjb; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=66cf89d2450b97444423768c9f7588c2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=66cf89d2450b97444423768c9f7588c2; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=66cf89d2450b97444423768c9f7588c2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3a0c7393194cc0cce2025a3929839ab0; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - g7woP75zJCm8SoinsqHE + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - g7woP75zJCm8SoinsqHE + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:14 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:14 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=e0c8ec03ffd43fc8624b5863e66afb9b; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=rREguFcIV%2Bkd6PZW9qRJcerqi7DSz7jvXgitd%2B84lDRmtuDnHEP0bMkWBwF9tAgZheZgnKAPxAfRafiXjYb1WMKBBc3qmeNajRxkrIpl5m3xK3frpO2R2rIdh6j2kCH3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e0c8ec03ffd43fc8624b5863e66afb9b; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=e0c8ec03ffd43fc8624b5863e66afb9b; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=e0c8ec03ffd43fc8624b5863e66afb9b; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d12d414ce6115d8f5b5d5a32b4669d55; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 2U9N1A1gUX2JpMSvRXQL + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 2U9N1A1gUX2JpMSvRXQL + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:14 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service_hide_inactive.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service_hide_inactive.yml new file mode 100644 index 00000000000..fb78d5b7a13 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service_hide_inactive.yml @@ -0,0 +1,2182 @@ +--- +http_interactions: +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Oc-Fileid: + - '00000831ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=377fef256a5c768f8a82fb789bab36df; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Etg5TygleRCdgcwk4fZzpUrATwIvn4wscLWgNFn7j2h4jWh5KX3n6nAn%2BEaE0EXPqYObh%2BAg4TWTfso51kU8TiD9g8or9GNFlp4pkRkotlEvf1JgSF8DGmBgsnzPxPtv; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=377fef256a5c768f8a82fb789bab36df; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=377fef256a5c768f8a82fb789bab36df; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=377fef256a5c768f8a82fb789bab36df; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9a2a4af0c9ea1ca4b902c756e70148f1; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - lu9Wni50oub9IgnnDqrN + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - lu9Wni50oub9IgnnDqrN + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=825d37f6ea715737b37976f492427c41; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=0JcX1HfaARMCIDDccW5uXe3yQAZEZBdguBoJRfBdlZLNWUofI3cn2IWT7MKKl4vHEl%2FjtmQ28p93HbEf1JB7hPbpqUrutkzrZEzZqtdPpMy1TD9IlMEXkZe4N9nG%2FA6c; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=825d37f6ea715737b37976f492427c41; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=825d37f6ea715737b37976f492427c41; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=825d37f6ea715737b37976f492427c41; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=aad9dfe90c25ae0023d6d4df1ff7fd5f; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - xvixXoEldUAQe2pTvxTO + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - xvixXoEldUAQe2pTvxTO + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '361' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)/8310Fri, 02 May 2025 16:33:10 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/831 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=c7cca5d8e5cce3d409dd353639ec8d11; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=waPYIIkytl4F3Nh0tKn0LSo5dOm%2FTUsRe4DrvKkjvmGYcWTED%2BpxsxLzi%2FBn90Rlqxo%2FxkyuROZeFcsdQagHtMh3qIEz4l%2BQKKrYhjL2cBxtTJiYx95nWZuXb%2BKu7o5X; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c7cca5d8e5cce3d409dd353639ec8d11; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=c7cca5d8e5cce3d409dd353639ec8d11; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=c7cca5d8e5cce3d409dd353639ec8d11; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a13c8a964c97efbbcf05b91625048886; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - MZc9IL1oq06e6XMfoegR + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '288' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":831,"name":"INACTIVE + PROJECT! f0r r34lz (-273)","mtime":1746203590,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/INACTIVE + PROJECT! f0r r34lz (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + user + anakin + 31 + 1 + + + user + luke + 31 + 2 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '682' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a79fc462cc61ccd32df48ad2eb35c468; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=AJHtMfw51rrYLZs2oF%2Bt%2FLGWmnZdxOGS5dBsYMzxZH3QBXoej53uFXkjE3D%2FkYqthk3qs1RMXT76%2BpNntQbqpFKqnDpVmvFMu%2FaHn7VCoBH6yhEJNS7bSAz%2Bq2JpVTfm; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a79fc462cc61ccd32df48ad2eb35c468; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a79fc462cc61ccd32df48ad2eb35c468; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a79fc462cc61ccd32df48ad2eb35c468; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=da5745d9a60748f6fece65db197ae607; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - bvgWM22wlsOtueCIJEfj + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - bvgWM22wlsOtueCIJEfj + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '395' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '192' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=82c50f706ca508900f13f3f31ca8cb74; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=faN2o1x2Q%2B4uPQ1U80u23tE4Gf4LLBru%2FnLHkFjqddqhf2U1fJm1rucrujTsEFiuOWkv%2FJCF%2FZoJcN9lVg7SUMxK1osCokd43lhP7ZX%2Fh1f5lS94qAokYOtUW%2BxAoMYQ; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=82c50f706ca508900f13f3f31ca8cb74; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=82c50f706ca508900f13f3f31ca8cb74; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=82c50f706ca508900f13f3f31ca8cb74; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d60ed7e4f7a6cf4be5f1506220f4e50e; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - lJS8iIzGsCC4wvc046tL + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - lJS8iIzGsCC4wvc046tL + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '480' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/103groupOpenProjectOpenProject311userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1)/180groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/%5bdev%5d%20Empty%20(3)/181groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)/831useranakinanakin311userlukeluke312HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8)/204groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/103 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=8d15e0d58914a81d0f5cdb327da79a23; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=giVy4vP%2Fbog7SRMVrHLE6bKOiEzUPh%2B%2BlUcbsvBybIHBPJH5hVevDjlzOqfZxuiI%2Fqv7UWlLu1yMGX1V6GULv7RmgAvLzVdbLmSyYXLPVQsbpOVF4zTiR0Cq9fsWXjkC; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8d15e0d58914a81d0f5cdb327da79a23; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=8d15e0d58914a81d0f5cdb327da79a23; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=8d15e0d58914a81d0f5cdb327da79a23; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=15055575f6a028239aa1d81d75526668; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - TcJLQ2MNjprT7CQFUJGs + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '250' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":103,"name":"OpenProject","mtime":1746203590,"ctime":0,"mimetype":"application\/x-op-directory","size":90335,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 1 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=c0a084c0e10297a1f738d22a1c3e0c11; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=NfcpB%2FhzhJNeTt77pP%2Fyq38bhx9dubc8c0sGABGmf9E7vgdE5wzzidlrs7eXkjeqYap65rCo1xy2qlG%2FtwTRi7mZ%2BZr56PWQ5bg2KtXSo8eCpr99JvVUocX8YbR6o6Vc; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c0a084c0e10297a1f738d22a1c3e0c11; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=c0a084c0e10297a1f738d22a1c3e0c11; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=c0a084c0e10297a1f738d22a1c3e0c11; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2ed10de80abf6f68a1f633a7342ba528; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - pWhWA2gsEg627ZQxKDxH + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - pWhWA2gsEg627ZQxKDxH + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '350' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Oc-Fileid: + - '00000832ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=b27aab3ab47b06e1b3bfe1eaf78b5159; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=iJbiZCfSuxD35pa3XikNVFzTTJoj%2BqjAdIWxZ5sUFWUctXoMaqFabj0ZqNJbZXVpodEhNBhpCnTgANWWcdzSVbQmg%2B8bMrzkLcKS%2FIGLy1OOAtl55d6iLNwtZ5f2Y6Q%2F; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=b27aab3ab47b06e1b3bfe1eaf78b5159; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=b27aab3ab47b06e1b3bfe1eaf78b5159; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=b27aab3ab47b06e1b3bfe1eaf78b5159; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fc4556867438f56a9fb4796efdef88c0; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - VLy9aBP2vQUQIkX1yGG5 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - VLy9aBP2vQUQIkX1yGG5 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=77478c98c1d9925c949e219cedc9b815; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=jAbuJ4AyWfiIk8yTD1QvzP2ydmm1Jcs2cZw4%2B7UYk0IitV6L9VdtgT3rbJX3facqGd%2B26cuhV7MjDV8SoA2yt3fTaiESil6y8ornte66T9SIPAJctj3X%2BvRXT30jOgPL; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=77478c98c1d9925c949e219cedc9b815; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=77478c98c1d9925c949e219cedc9b815; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=77478c98c1d9925c949e219cedc9b815; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=790552c5ebe8c644a9d8947de1f5457b; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - Xr9DLLCE5DTxWzgT9nDV + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - Xr9DLLCE5DTxWzgT9nDV + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '360' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/8320Fri, 02 May 2025 16:33:10 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:10 GMT + Oc-Fileid: + - '00000833ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=20b04dc669a5d104031acd674a80cdb6; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=dcRoBUKGvn%2FI1VsT%2B51%2FHnxCkq%2BApo80OWWH1K8KJ71XDVAMP7RlQ8fJYEZcD3XWcR8IfGaGUu7ddTCQg0VBsnRTXnaVSmt8cyziByB9Yo%2F20hARHqX2RzTzx8tbnxFt; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=20b04dc669a5d104031acd674a80cdb6; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=20b04dc669a5d104031acd674a80cdb6; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=20b04dc669a5d104031acd674a80cdb6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3bef8b12cf31e9539546623d57ecc023; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - TRvxCQhEBcF9cuBPrSn3 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - TRvxCQhEBcF9cuBPrSn3 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a06144d5a6ecf53f2da70d8a14b8b3c1; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Kfm98jNbNco2g%2BcsUB0yVXJfKRH76%2FR5sVZLVSOLif9TGkl8hQC4rOzx%2Bz7lwIaZHjRAZDqpMJtlQRxmAF0HzIU1QLP3Y%2FZiCFNJ84bJBEstNH1CLIgDAWsXON8XFeK0; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a06144d5a6ecf53f2da70d8a14b8b3c1; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a06144d5a6ecf53f2da70d8a14b8b3c1; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a06144d5a6ecf53f2da70d8a14b8b3c1; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fece5f4e8b4c3cd595a124a088aad3b5; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - S6lnouajYDK3qmDjaP6e + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - S6lnouajYDK3qmDjaP6e + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '366' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/8330Fri, 02 May 2025 16:33:11 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Oc-Fileid: + - '00000834ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d64c6407a19246892c71fb976c158be1; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=DVzuKGGnxUaisTvpauzZ84xUkKuKEtorWVihQMKYukO%2F5GAgD70VATVZe7XtchjkdYGiy%2F6ImQG4F9cCOw390r83HmxmgvfM%2FO9PnctpBG0cPBLfLqg21rhKJi5VDgYo; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d64c6407a19246892c71fb976c158be1; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d64c6407a19246892c71fb976c158be1; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d64c6407a19246892c71fb976c158be1; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=b1031486d9f1bef7db3bee1a4fb38430; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - dgtZXZxuv1Rk9akzcbHB + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - dgtZXZxuv1Rk9akzcbHB + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=903d641744f31cb27b322645ff8fd583; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=aYo5gMpeuRalyFwiF%2BDP5Lr7ndYPGjbeUDE%2BJZIpdGSaChDgIYDHyMkGgVh20MAmn8EmKo0y%2B%2FJ0qFexGnKgC%2FlOY54C7pAb74Sm7%2BlgDceRTLomTiO4oP%2Bi8faZyjzW; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=903d641744f31cb27b322645ff8fd583; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=903d641744f31cb27b322645ff8fd583; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=903d641744f31cb27b322645ff8fd583; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=bd20fc9cb2c0a244714fd81a86ba7b81; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - bMk2r3o9uQzhxRnLt0MR + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - bMk2r3o9uQzhxRnLt0MR + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '346' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/8340Fri, 02 May 2025 16:33:11 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/180 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=b434921338c31ecb998727c3be04da8a; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=h%2B5ZBNQhiwCNbOcFajpLOn%2FU%2FE%2B0wsLk9weLmwHHKP06s6pQXmZvPP0cNRDjoXumCIhKppYyji7Gn1SSFEe5DAS5k0O8AOVMWxwSk57BlGcNfAU4t1iwIjWSvDS71EnL; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=b434921338c31ecb998727c3be04da8a; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=b434921338c31ecb998727c3be04da8a; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=b434921338c31ecb998727c3be04da8a; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=34cf8c218c51b353cc5ea2bfccce8530; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - YCnNCdz3k4FEKugjpx46 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '267' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":180,"name":"Demo + project (1)","mtime":1736432335,"ctime":0,"mimetype":"application\/x-op-directory","size":924,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/Demo + project (1)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=342acbcdf1bf17a61af4383c167ed26e; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=PqTemvW4b7aR%2BM%2BiiZ6YKAegvgypTOnx4iEF0fwEoGhoisY3lODyVmAfqrokkPdE4%2FiCcovUClP1GQd6WybmU9XUuey%2FGbyoMLdvoDvB9WJZeyjMOPsCZ6bh8IO6ENag; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=342acbcdf1bf17a61af4383c167ed26e; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=342acbcdf1bf17a61af4383c167ed26e; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=342acbcdf1bf17a61af4383c167ed26e; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a0c83f01dc73fdd6b067392a0171dfde; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - sKzVj8CQ8VSnnq8dbVxU + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - sKzVj8CQ8VSnnq8dbVxU + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '371' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/181 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=7a74ac571a5cc99c0ed4788324b8e3cc; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=9FFbP3QLs3Nx%2BTPGw9HkaplWJyGDVc4AX%2B%2Fh7GOudKmJ8wVxBQcsaVDj%2FimnO5M7uFiPYB9mawGpOGIqfIhFzfbu6ZwtrJgF7UpZ4YF7dfy%2Fa7uj7SayFLpIZElG9g%2Fs; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=7a74ac571a5cc99c0ed4788324b8e3cc; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=7a74ac571a5cc99c0ed4788324b8e3cc; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=7a74ac571a5cc99c0ed4788324b8e3cc; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=985cc8f8cea1fcd135bfadd4646ffd7a; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - riDVPU4fjYZRLuV6Pxsd + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '267' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":181,"name":"[dev] + Empty (3)","mtime":1709925816,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/[dev] + Empty (3)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5Bdev%5D%20Empty%20(3) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1715f6ea116e3e25e2eb97476bbcbb79; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=i%2FIT8kP88UShNG37w9z1rgQOcVayI3%2FAgne59vAe72lmHYXYeyLNNhlXZ%2Far1HDM9%2FmA7z157nBrAutE2KdKsEZgoscotq%2BxcdT6aD%2FkuVvT7132XU9Pbs04YVxC69KZ; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1715f6ea116e3e25e2eb97476bbcbb79; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1715f6ea116e3e25e2eb97476bbcbb79; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=1715f6ea116e3e25e2eb97476bbcbb79; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=febf4157a20f58f59d04c24db2f4d2f9; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - lihnyO9WPxihkDnih97I + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - lihnyO9WPxihkDnih97I + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '374' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bdev%5d%20Empty%20(3)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/831 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=bf804b45b4579c4d4411cc148d7d7673; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=KcJbMJbwjDBri7YlRFgVJzHXi4cy08lyY6EBbzOnKwK3foKyrvu0FBD005gmusYtCBQ99uDCRCxRCfD%2Fi%2B2pCOo8eHlMezRsbgTAA6%2BzRqS28S66cjH5wA55NTGc8YRp; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=bf804b45b4579c4d4411cc148d7d7673; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=bf804b45b4579c4d4411cc148d7d7673; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=bf804b45b4579c4d4411cc148d7d7673; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9d6d33d29bf2f20056b841ec3ad3138c; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - KDq32ZTyf6vWetioKCdo + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '288' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":831,"name":"INACTIVE + PROJECT! f0r r34lz (-273)","mtime":1746203590,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/INACTIVE + PROJECT! f0r r34lz (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=884a4ba330597de3072a2926f70aa500; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2FJHhfPiJ1uxiwxZsqWBdCQdVwz3JJERso6rhFrxOYMV%2Fu0vP8U4AwBDLeelqdh6J2x%2Fg%2FLZDfcrUK97oEZdOvIxPI6S59Agg8AGXxNY9ZHY5bMfYotW5NkR9fSUZ3FTB; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=884a4ba330597de3072a2926f70aa500; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=884a4ba330597de3072a2926f70aa500; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=884a4ba330597de3072a2926f70aa500; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=95d7313973751361d5841a38198bf51f; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 7yd9Ck2bSzjS21mjPFSg + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 7yd9Ck2bSzjS21mjPFSg + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '395' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/204 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=23e681929e72249ba25437a6f76a70db; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=7%2BliE%2FFGNNxQGYr2hNn9ayG9hsNbvz6OpQ6cKbyTLRQE0ZutMRU%2Buhl5mtokTWyEVDFDULj1zTJEakjNHW8zCiyiJX%2F8yhbdyZkhLCCxpbck0%2Brxrrrug%2BIKCr6ol4i2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=23e681929e72249ba25437a6f76a70db; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=23e681929e72249ba25437a6f76a70db; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=23e681929e72249ba25437a6f76a70db; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c859fcf32550ecec92877b6e7abf6381; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - ltYgFpCqd7aAATbFg1cb + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '266' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":204,"name":"Projectify + (8)","mtime":1736255538,"ctime":0,"mimetype":"application\/x-op-directory","size":89411,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/Projectify + (8)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:11 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:11 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d6a5dad4061e315803a14e3f298f4a7b; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=gnw9GpzrEnYUH41pvgDfkWGQC9XS6k3NTJeuhNBacyWu3tGF%2FvahquDTwxnKHnzlxV6MO0Y0I3SKEJ7kKFNZkL8Hd2LaC2Pi0m7X47WqM1ql4LWzkYtctpk74G0Fwlz1; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d6a5dad4061e315803a14e3f298f4a7b; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d6a5dad4061e315803a14e3f298f4a7b; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d6a5dad4061e315803a14e3f298f4a7b; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=bf98ea9b1e4df81f9c4dc2c093fc5c7f; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - ITGevASaR0LrGeN4Qr8f + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - ITGevASaR0LrGeN4Qr8f + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '367' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:12 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '141' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:12 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=ef329c77c6e2e6e4ba9a51b416beb1a7; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=8QJoCSqxQX1PS45RWeFpo9MLu1llIEYlzLeb8flPBHAMr%2B8QHthJXp8YIPQc0C4iX3krpbIjC1iyC20lnEL7nk%2BD30ayPTZDH47%2FFV0h7xNAI48%2B%2BAXdgGEbglC01QSN; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ef329c77c6e2e6e4ba9a51b416beb1a7; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=ef329c77c6e2e6e4ba9a51b416beb1a7; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=ef329c77c6e2e6e4ba9a51b416beb1a7; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9b011238c9e067ef98edc668eebe1317; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - Q6Tlpj7qdDyFdFfoDDLN + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - Q6Tlpj7qdDyFdFfoDDLN + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '351' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)/groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:12 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:12 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=3c4c4219b5dbe7dfcc8d39037ef8c325; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=3IOzw%2BLNpdhMzfREqrAH9eug%2BV23Mva7QtaWhrCZfGCiElWtY7klUUzcH4owUvN%2FNHhnRCpTGFbijZgvAskd0HKLG44hbjgGp98nj9WVcIzaS%2BfWPrsI0fswfqDvwzXI; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3c4c4219b5dbe7dfcc8d39037ef8c325; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=3c4c4219b5dbe7dfcc8d39037ef8c325; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=3c4c4219b5dbe7dfcc8d39037ef8c325; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1c37634355358c73ab6ccd5eab922302; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 3HC6Ipos9lmkkQTrHcY0 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 3HC6Ipos9lmkkQTrHcY0 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:12 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:12 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6a402072875e22ab12e15c4f4ea13bed; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=cAYEm42cTjS3P8v21fqf0iEAUavY0sWCzfOlVH8b0N%2BmESwiyoXnZw9Cqhhvto%2Fs38QDW%2FJzGDEW3RlEw7jLjgM2Mo%2BUqPsjQ%2BdKHE6COIp7oM%2FqyciB7IzBwanSs4Ii; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6a402072875e22ab12e15c4f4ea13bed; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6a402072875e22ab12e15c4f4ea13bed; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6a402072875e22ab12e15c4f4ea13bed; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=72eb9d86676c2b36b47c57342b85ebc9; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - VlimAxzZZke3fVNw98ms + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - VlimAxzZZke3fVNw98ms + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:12 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:12 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=285da171cc5d2c76f3c8b499729ca176; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=bzSbNf1aR9eU1LYpvhKISeTBo37w7vmKsjOfVV%2BNeJFX1HPqnB3ucZOI1ncaTZSlHS7dhK5DdPw1nonrIG7PLfxHqCg53bVGYBYrzM%2F3noaQqGpZuz5ir%2FPAmXwRKtAK; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=285da171cc5d2c76f3c8b499729ca176; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=285da171cc5d2c76f3c8b499729ca176; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=285da171cc5d2c76f3c8b499729ca176; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=671018fc1ab14e82d3a17bba7a4bb0d4; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - xhSgyfaq6IcEbUj3UwQC + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - xhSgyfaq6IcEbUj3UwQC + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:12 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:12 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=be0e865b75ed9ddad807d0a2fd9c443b; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=g9LaXnvj1czgy2kSIXFmqs6asx90k64Fiey%2B%2F4xHMlCOO8EXSeYqWEsxxM2QOSDYzCDAkh0r%2BNTgGu7B3mto5BGvg7tjtqk8GBCh8QBxVCyOvX5F68OBq%2FNDUKuCzMU4; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=be0e865b75ed9ddad807d0a2fd9c443b; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=be0e865b75ed9ddad807d0a2fd9c443b; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=be0e865b75ed9ddad807d0a2fd9c443b; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fd48eeb1c6e65c2b39790f4461066f98; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - INVCVP57tLgY46zxLEYE + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - INVCVP57tLgY46zxLEYE + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:12 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service_rename_folder.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service_rename_folder.yml new file mode 100644 index 00000000000..b9e8192f1a8 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_create_service_rename_folder.yml @@ -0,0 +1,1834 @@ +--- +http_interactions: +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Old%20Jedi%20Project + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Oc-Fileid: + - '00000828ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=29b1fe3ec01e3cfda449a6008b24fff6; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=AwXQRZpgIG0cRRTXOvzObS8IO2dNLM5aqJLQZGf%2B6%2FV7KAMzWuWEP3CIEYD%2FBdPyB6o711owOS3xsl43KtUzQS70QhGqD5dq0GzIXutl47Wzjq2ss1TGgP1VH1oMbE39; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=29b1fe3ec01e3cfda449a6008b24fff6; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=29b1fe3ec01e3cfda449a6008b24fff6; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=29b1fe3ec01e3cfda449a6008b24fff6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=495e66f0699ee9e4000c18088c284011; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - QsKgMMcGqZiylO4VMKqK + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - QsKgMMcGqZiylO4VMKqK + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Old%20Jedi%20Project + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6480ce6003243ec93a8fb9e99eef6af0; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Kjtz%2FREWjnhtATkOdJr2QGJwca06hZl%2B58Qsc0lIEaUTNP8R%2FUbG2cH%2B%2BeD0iL4zkjFwV8j703s5WZQcz4EgWSaJpD6%2Fv66i77H4nz7Lzc4DM6OGQ6D2QXxJkwPttLu3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6480ce6003243ec93a8fb9e99eef6af0; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6480ce6003243ec93a8fb9e99eef6af0; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6480ce6003243ec93a8fb9e99eef6af0; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ed03133c349d6a8e9ebacfb0f4462015; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 7a7lqyrVBcxDAeY7CmWR + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 7a7lqyrVBcxDAeY7CmWR + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '331' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/Old%20Jedi%20Project/8280Fri, 02 May 2025 16:33:08 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '192' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=0189af8f4c466b482d70da54597a1ebb; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=vEVOqh9YdMno1FmnoR0UKabQ1bA0yJGowpHwHORf6iAwntjQe9N2NczzdSf%2FsHKkXMSJ0j2Px%2B6sBPXC%2FUv1JX3kW3k%2FUdyqcID72oo%2Ft7Jl0Q817x2a0ydvRdITFiOM; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0189af8f4c466b482d70da54597a1ebb; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=0189af8f4c466b482d70da54597a1ebb; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=0189af8f4c466b482d70da54597a1ebb; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6b841f25ba28b93a2769e2a89c1bffc1; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 0BlnIsgS5XJghVVBnN6R + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 0BlnIsgS5XJghVVBnN6R + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '446' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/103groupOpenProjectOpenProject311userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1)/180groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/%5bdev%5d%20Empty%20(3)/181groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/Old%20Jedi%20Project/828HTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8)/204groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/103 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6f3c4c571d851824327c9ac5b07c8d71; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=pT1sfPrjC2S84aH9XmIErjnE6pz6PITIGQ5EMci9fr6j1fiVVz3qx%2BH0Yik3fObKizv2Y44e2SPohzXbDWWQ61E5Vkf0E1YYiSyxH9D9sDGMX8jyjSntpVah7fRoQVEC; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6f3c4c571d851824327c9ac5b07c8d71; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6f3c4c571d851824327c9ac5b07c8d71; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6f3c4c571d851824327c9ac5b07c8d71; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=655395d87e47b54b19d0314e96d1cc5b; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - P7HJQdT6VFfy6pwngNxT + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '251' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":103,"name":"OpenProject","mtime":1746203588,"ctime":0,"mimetype":"application\/x-op-directory","size":90335,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 1 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9a00436e10346cc07f2213e045acf62a; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=twu7a9t0nEoe1LxyDXnMJbL1ZyPhZWY6tvec4xPvQ0uENTjmVv04l7LhB3LVwzV3N9w99VebH4ok%2BpIJpw8iACrJ%2BEQW%2BuAa7%2FXfb2uBxGm2xVjyIPV81ZIxRnDuTekR; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9a00436e10346cc07f2213e045acf62a; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9a00436e10346cc07f2213e045acf62a; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=9a00436e10346cc07f2213e045acf62a; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=25b7c22c71291c87c1a27ed78f70788b; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 9ESUEcmtjA26COHxczEj + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 9ESUEcmtjA26COHxczEj + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '350' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Oc-Fileid: + - '00000829ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1fa54e34083eaf56c4d07551fb096f7d; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=r43S%2FiKvN2wXSKyCqC2JUd%2F2onRtCDQkHQ%2BNJ%2BDM2DlDGgrRZ4ZWnBb8%2FBairSfPY3Uygvk2hphAT4E%2FS%2BS%2Fjp0UqGSsxW%2By7zUr7vbsXGMp%2Fx0uDbeMzkMcIJ0Es7Dk; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1fa54e34083eaf56c4d07551fb096f7d; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1fa54e34083eaf56c4d07551fb096f7d; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=1fa54e34083eaf56c4d07551fb096f7d; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=918949f1da22b8865db7d9c37ddaed8f; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - fAlk85GL5dUPUj8SkYXP + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - fAlk85GL5dUPUj8SkYXP + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=38980e3dfc732ae9117799da76e257d5; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=6f2YdeQNpA1%2F1nibG1VDU2f%2FO5JR6aX8rZdkzgwxSxoHVhWlMhsChu%2Bng3fe2q4iKLZbtoFbzrUh2W5%2FiyjbpEa4oOuRNr3f%2FF1np3Z3F9CJjtG5pRS7uhkcS%2FW6GUTO; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=38980e3dfc732ae9117799da76e257d5; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=38980e3dfc732ae9117799da76e257d5; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=38980e3dfc732ae9117799da76e257d5; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6ab43d5ba811d1544025c32ea313594b; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - zHKMNPBKEZfCGuMgyEYK + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - zHKMNPBKEZfCGuMgyEYK + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '361' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/8290Fri, 02 May 2025 16:33:08 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/828 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=996e94ec46857de438292da35c52c7ad; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=FLD3PR96DoejitBaFwTaNtm63aZBJ193b74nvJpZHMe4oPshfgyQdN1wApsDw%2BLWILD302xUaw2z%2FZjbI9hULLtGbt9CYr7MtjN6Flu2%2BYcWtDq1SbU1oQgWL4fWRcsq; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=996e94ec46857de438292da35c52c7ad; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=996e94ec46857de438292da35c52c7ad; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=996e94ec46857de438292da35c52c7ad; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=4f8c7ea41f8bfbb00f4c7de061d58391; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - tvcYnTm3SiaUhZMiFxLy + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '259' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":828,"name":"Old + Jedi Project","mtime":1746203588,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/Old + Jedi Project\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: move + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Old%20Jedi%20Project + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Destination: + - "/remote.php/dav/files/OpenProject/OpenProject/%3C%3Do%3D%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20%28-273%29" + Overwrite: + - F + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Oc-Fileid: + - '00000828ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=296fa0806296a8f92196f48a8acff92b; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=GmJcI9YZ2m8QOwSPOc0ZGcgZIa3LXpdtJVpt%2Fq%2B%2Fn%2FiPEboGH4kRswFssPQXsJ%2FIBvvIOtsO8H4revftXXTTYZ10gXvcBcQQanVx5h%2FpQGk72Y6a%2FkJ34Ef7RLMLne8q; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=296fa0806296a8f92196f48a8acff92b; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=296fa0806296a8f92196f48a8acff92b; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=296fa0806296a8f92196f48a8acff92b; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=4d8af074d9b3a4148153f5a8d6b04d3c; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - UbTlulCWYL8nOtexMdDD + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - UbTlulCWYL8nOtexMdDD + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/828 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1e8ea6ea4a69ac6216cd54b280d80dd3; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=FFptjhLNSWzQ30swsALmbtcyU6vr7x%2BTFmY4%2B61LxI7HVw21VqwsgZZjafgz3zD1zBRNJspEMxsmBCNqU9Uiahmj9KHbHv0PNT7POM5CDuusgz9a6R7FOphmaOnUTRcV; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1e8ea6ea4a69ac6216cd54b280d80dd3; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1e8ea6ea4a69ac6216cd54b280d80dd3; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=1e8ea6ea4a69ac6216cd54b280d80dd3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0dc60de769b7b224eeb2f1e5fa58d485; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - j0JAxPYHf512z9yL6Fca + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '293' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":828,"name":"\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)","mtime":1746203588,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:08 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 16:33:08 GMT + Oc-Fileid: + - '00000830ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1bb4fd766a383f23de23b3c4dd579548; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=xa%2BJG0il5JWun7wXIzxOC0ncaQ6IBJ9ocMo425RjRWz7k%2BTrj0XTw0ZlkRNTEPSLqQgQRfnRXBu%2BcvY4Ado6eLZVw00CXopo1H7vWuOp50523XxuqcNiDYk7YaP%2FDDP5; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1bb4fd766a383f23de23b3c4dd579548; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1bb4fd766a383f23de23b3c4dd579548; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=1bb4fd766a383f23de23b3c4dd579548; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fb95ae5427dc0d0535d2cde60c3c0d79; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - x3s3kJnjcg7sNp0REE1T + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - x3s3kJnjcg7sNp0REE1T + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:09 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6f8824a9a6b273c6235ef58c162591b6; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=1juinDR3orrikPfH2Tvk8A0SUeSdvDbRaSEjqAhJeDz8CHXpfxz6uGlJ3mQkxZXm%2FHY%2Fwd3dOWX782A4xieFS9d%2B69a%2BSa96nf6Mh7EBB4m%2FaEamJHNwZSe%2BHGL38MES; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6f8824a9a6b273c6235ef58c162591b6; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6f8824a9a6b273c6235ef58c162591b6; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6f8824a9a6b273c6235ef58c162591b6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=08e72e03c03eeb26df9ff4bfad2ab4b5; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - wTD6lH1rzxxhUfsoB1Jw + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - wTD6lH1rzxxhUfsoB1Jw + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '345' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/8300Fri, 02 May 2025 16:33:09 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/180 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:09 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=67dcdffd9a036a97de526976d9eef72f; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=4HXcdzrVMfgfNZ45mSg9PV4v8%2F4yO%2B7%2FNiwL81WsvT1GaIAfhY3wPRqJGwserHAly4DqTE5Yw1iZKdq0Fu08juMa8q9Lz4%2BCy3Cd9r93T3qa0a%2BOBDTC1aLLodZ88%2F9E; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=67dcdffd9a036a97de526976d9eef72f; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=67dcdffd9a036a97de526976d9eef72f; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=67dcdffd9a036a97de526976d9eef72f; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e5a6612fbc5458c01c032717c9447289; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - nqWrYQuhSYomxTJsN3sU + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '267' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":180,"name":"Demo + project (1)","mtime":1736432335,"ctime":0,"mimetype":"application\/x-op-directory","size":924,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/Demo + project (1)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:09 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d4013d0b1d10d16e928d1f21403261d1; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=irfMA1ONEOeOLuUDyKcjYe7HMumxPc638cGIHBsrJ5wow6j2JI0IDD2Q9z2FabvpwH48mLEpeOprDX7KT4UBHAhwwCsITWguVppjLPPUnYQI2Lt7OGythhqrpAvVg2hg; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d4013d0b1d10d16e928d1f21403261d1; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d4013d0b1d10d16e928d1f21403261d1; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d4013d0b1d10d16e928d1f21403261d1; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e9628f4a15315b395a2b8a226163c1bf; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - oUgWVJDZJoLekwVnGymN + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - oUgWVJDZJoLekwVnGymN + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '371' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/181 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:09 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1f0d62fa292ab53f529290450a5473a4; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=31bbHFaam7HjHcFir32vnUkdOcQMzlULFkL3v6fVeShf7Jd3ZpFZgfuSiD4btpl9Gk2fcrOZw5Mj9Pm64wTWuet%2Bs1CDKj9QhdKZU4ut%2FNAlWOrpTMbVZ%2BnGefWusYP8; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1f0d62fa292ab53f529290450a5473a4; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1f0d62fa292ab53f529290450a5473a4; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=1f0d62fa292ab53f529290450a5473a4; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c1dba1b2d2c75282fb9f14874840a91d; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - UgIur65gQTjWVl8GlKsc + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '267' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":181,"name":"[dev] + Empty (3)","mtime":1709925816,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/[dev] + Empty (3)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5Bdev%5D%20Empty%20(3) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:09 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=912eab7d16ae102e57b3c9c7b61898d6; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=tA4VTLwmaqxKY9xqtElL6e%2FdffRvSDYTaXGvpIM0yoKS7Q%2F%2FPqzJchsIt5UpKEr%2BrhjmrOLhymg7znlCKoBAsO6DOSu4W9iHThGkB%2FSnOvFHb%2B5xWCVVCufNwRPJ0PWZ; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=912eab7d16ae102e57b3c9c7b61898d6; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=912eab7d16ae102e57b3c9c7b61898d6; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=912eab7d16ae102e57b3c9c7b61898d6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d7435bd5313822f3ee5c76219b707528; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - SlcFAD94NPTMndove4Os + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - SlcFAD94NPTMndove4Os + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '374' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bdev%5d%20Empty%20(3)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/204 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:09 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=03bef3861765fdb40ae53f7b6f4c5ec0; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2BNNGN7n5836f3jFFQXr6lr7%2FPQq4PswaOSN%2B2Omq7j6Zg4%2F0S9uSXMoMmNCLhkryFn4UHCqhAhwIucMVUiyw%2BqdqRVKEooTxaAGbAVhb%2BOZ5Q9yhSZ4bLIGHc7coizx5; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=03bef3861765fdb40ae53f7b6f4c5ec0; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=03bef3861765fdb40ae53f7b6f4c5ec0; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=03bef3861765fdb40ae53f7b6f4c5ec0; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=5b48f0db3c0652adae99857d2bdaf9ea; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - QQEWIe8MJqQYkmIPLHbT + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '266' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":204,"name":"Projectify + (8)","mtime":1736255538,"ctime":0,"mimetype":"application\/x-op-directory","size":89411,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/Projectify + (8)\/"}}}' + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:09 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=bf33805800c053645738478656e70280; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=H5Gq0wAnIoRRIXsNpPpEqDU8IiFp326Hc%2F3mfmBqA7wDwDEnlsQTOaLIVaSvWFvEIrfHDO%2FIJyLBcgvIBQaUHgkJyJodho9aKOROQDOFi7pPfk%2BsCPVNR24V%2BHP11PAX; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=bf33805800c053645738478656e70280; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=bf33805800c053645738478656e70280; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=bf33805800c053645738478656e70280; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ea6f6bf88c352d0796806a023d4c05cd; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - UhMRF45GQLSrS85HsoW3 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - UhMRF45GQLSrS85HsoW3 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '367' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '253' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 16:33:09 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1065ce329b6a4211c399ceca4f3826d3; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=4BTrHnvjiT05VcBgKX5KWImNCQdU1Sujhh72LbWKiA5rYYPI0pi4o2t%2B1%2FI4Eg7xuO8tkzCbPLEbgSU6rdrT1sFmsPz2GFv%2B3c%2FVNMTJlL3kzUlv%2FIdtectDAGFfdthO; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1065ce329b6a4211c399ceca4f3826d3; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1065ce329b6a4211c399ceca4f3826d3; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=1065ce329b6a4211c399ceca4f3826d3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e171d4bfd144f1a3ed4c1812473a4af4; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - QQ5AzzzjehraIPMQPdCR + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - QQ5AzzzjehraIPMQPdCR + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '609' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/10390335Fri, 02 May 2025 16:33:09 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1)/180924Thu, 09 Jan 2025 14:18:55 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/OpenProject/OpenProject/%5bdev%5d%20Empty%20(3)/1810Fri, 08 Mar 2024 19:23:36 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/8280Fri, 02 May 2025 16:33:08 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8)/20489411Tue, 07 Jan 2025 13:12:18 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/8300Fri, 02 May 2025 16:33:09 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OKHTTP/1.1 404 Not Found/remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/8290Fri, 02 May 2025 16:33:08 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OKHTTP/1.1 404 Not Found + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:09 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d88a6df7e8ec4ab0422ac3b8f270966e; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=fSvTaPAMaFr9PJcFb2Nn00mkU3rCCBeAWgo5Z3WDhvVWEoQHIqbomI3%2FWb%2BEhbExoVyCplyx92XCRiEHocTJpn172opmIyHKrCRAFC4QL21B14tUjDd4ZC0s6R74jYQv; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d88a6df7e8ec4ab0422ac3b8f270966e; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d88a6df7e8ec4ab0422ac3b8f270966e; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d88a6df7e8ec4ab0422ac3b8f270966e; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=bb77846c8825ce0e620b2b3b7870f338; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - N315wzlLfnQi4HtK3Etl + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - N315wzlLfnQi4HtK3Etl + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:09 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:09 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=f0ae4b7c8f0f1bc9126884bc2b065960; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=gpq8RS1a7csH5705yxH5lebwPMZt2lHNhj9tgK1kXdgol7TS7XS5vYf7EGkTUwN4UaBYsEbep0%2FHW3Kx8atFVuPe9%2B7dTRRhRE%2FhaCYZuqVGeKNKHs2Fby9giW%2B7VdeA; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f0ae4b7c8f0f1bc9126884bc2b065960; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=f0ae4b7c8f0f1bc9126884bc2b065960; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=f0ae4b7c8f0f1bc9126884bc2b065960; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e6d66aa69b95e711b4a071eb02a076a3; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 0L2XUWYeXGHpxsr43N7w + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 0L2XUWYeXGHpxsr43N7w + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:10 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 16:33:10 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=13c0f94fe9289cb2813079e3fad8dfc3; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2BXBKTGTT59Zd6q%2FvsJLYbjTxxmnIN7JhTuxrTNb%2Fab10E87GIhhoru4XSWXb7SC05bN2BtfbfFSXpgAG%2BRumozZ3M475I1BCaZQ6OUNS9aPc77e8dvxjTG%2F9b%2BKrMHxd; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=13c0f94fe9289cb2813079e3fad8dfc3; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=13c0f94fe9289cb2813079e3fad8dfc3; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=13c0f94fe9289cb2813079e3fad8dfc3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2bc5243b414c5003d0f08a04fea25c9a; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - HSL2b1FldzUfyLfjHem1 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - HSL2b1FldzUfyLfjHem1 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 16:33:10 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions.yml new file mode 100644 index 00000000000..69ddb3ecbfa --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions.yml @@ -0,0 +1,1886 @@ +--- +http_interactions: +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:21:08 GMT + Oc-Fileid: + - '00000864ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6e1f4aaeee971af611144c56d6f30237; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=JQ%2F1XhyRoIL6807A92uAH4JFvJkegf4XXY00RH7mMECkuSrzEX3kWP5vqHEtRIqIF4BipVoBuxYxp%2BWec%2FlIZ7kw6cByz9CyrM5hB6SaWGfnQdTkW%2FnmTl7ekfy4QkN6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6e1f4aaeee971af611144c56d6f30237; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6e1f4aaeee971af611144c56d6f30237; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6e1f4aaeee971af611144c56d6f30237; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=75000f9f959efd80efb8e378cfe163d5; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 8dTS9KYgy0VyW4GSsAtN + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 8dTS9KYgy0VyW4GSsAtN + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:21:08 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:08 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=5f008cf23579768f26cbdcfa2d660082; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=JElRRS4zNehT82Ua2IukD16smdbPotmgeiRTdpVumHyxtkrGbxtB%2BZwADwKLUbAbxccSnCjCwYwLUkcnzQ9k7l8%2FGzkxUhG%2FF5kf9ycXBTFjloQy5I0qs3PW7unCRLNs; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=5f008cf23579768f26cbdcfa2d660082; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=5f008cf23579768f26cbdcfa2d660082; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=5f008cf23579768f26cbdcfa2d660082; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1e2075707b812c41d87fbddfd372495a; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 3FQ0UtuYe2ARhRBFWmjS + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 3FQ0UtuYe2ARhRBFWmjS + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '361' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/8640Fri, 02 May 2025 17:21:08 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:08 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:21:08 GMT + Oc-Fileid: + - '00000865ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=4ed6bdb85cd55649b7b856085eb82368; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=MmYd%2FUklyPJN%2Bg54YHLa92RgBcnRb9hEPjjGFOTR7nY2c5SiXUNgOp954WpnelkrwW5aGF6LSY5qKG%2FH5k0WjfD0py2izQ%2F9nQpPkLXdPfLtAII5j117xINrdBvYi2Cc; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=4ed6bdb85cd55649b7b856085eb82368; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=4ed6bdb85cd55649b7b856085eb82368; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=4ed6bdb85cd55649b7b856085eb82368; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9b2eb72ad842c99adfa316d073b00a76; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 5ThuhwAXvFvJGj0wX1IK + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 5ThuhwAXvFvJGj0wX1IK + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:21:08 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:08 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=246bbbea288354b7968c29f3a4b6d213; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=nB95oFMDwBv3VRRSrAwIO5wAPm1blc%2BPxzd7FyaMEe%2F%2FyjsBfbFy9E57FMSM%2FkfccAb0GWNbejcyHMsyPSAdo5%2BRrG%2BFTdNE%2BLDLXK69NlzFZxvfNoHruEEj5D%2Bqe3dM; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=246bbbea288354b7968c29f3a4b6d213; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=246bbbea288354b7968c29f3a4b6d213; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=246bbbea288354b7968c29f3a4b6d213; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=7313dbc5dce39028dce81ca6a1b05b12; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - QwwVOFGgSyrIMapD5YUt + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - QwwVOFGgSyrIMapD5YUt + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '367' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/8650Fri, 02 May 2025 17:21:08 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:08 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:21:08 GMT + Oc-Fileid: + - '00000866ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a5501ac5ff45095b7189fcecd65afe1c; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=B6Ph%2Fhn5RsTQuElDJt%2FhPG3RGvzz8xBdxotc1EsS%2Bh4Bgx5y78rC9bzUvuDqeY%2FRfzGTtCct47opbk%2BReJhzWpytuvZHzxUSyQJp2er917AmizuUq8DDP%2B7GqY8WfrZh; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a5501ac5ff45095b7189fcecd65afe1c; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a5501ac5ff45095b7189fcecd65afe1c; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a5501ac5ff45095b7189fcecd65afe1c; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=77eb28d44534aac1ab660e59c22559cc; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - NDMJssSfqrNJEjfpNrvU + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - NDMJssSfqrNJEjfpNrvU + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:21:08 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:08 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=cf25a199d33a89dd0603454dc54dccd5; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=V6X9k5YDf6pcBHDzo4Has%2FmcQOmuYqxnEwIVdh3vJZ1zRmxtkAl4gNwkNVFSnTQ7Ev5HmykW1ijJID3zqEb4j4WWaycy8%2BWrw8tqPwUdsZgmDOrApVum2CQANDBdpX1%2F; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=cf25a199d33a89dd0603454dc54dccd5; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=cf25a199d33a89dd0603454dc54dccd5; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=cf25a199d33a89dd0603454dc54dccd5; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8742147221fc89ba00a7d9d652a863ae; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - cF0QgltwO1WnnpPE8mIm + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - cF0QgltwO1WnnpPE8mIm + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '361' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)/8660Fri, 02 May 2025 17:21:08 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Oc-Fileid: + - '00000867ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=ea6a0a997679b8943f4bff7af44994c2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=1T36nzXioEjuP6lynaQffDH0NPzipTl0Bq0vYw3ZcGoyxfcW5%2F0Z0Wvw%2FebRkCZkISjYtFe4S2IEg0XKMGC75yXNM6bem7naWLQHTfeEb0VTlAsXHXIwZEzOYuX0tAsa; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ea6a0a997679b8943f4bff7af44994c2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=ea6a0a997679b8943f4bff7af44994c2; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=ea6a0a997679b8943f4bff7af44994c2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f45991e982b935f26b00f8843a8aef1f; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - T6amLeAIx1aafqg55kIV + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - T6amLeAIx1aafqg55kIV + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=7891cb3b736eef30fc25fee31da97ae8; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=JMRSzVVNuJ34erXiwALm0ZB3ri3n1wnmN8CzArXGcTBbfDpzHe6KyZ1FSHmaCqH9QYU%2Fd5jC2EfxJRNRFW%2B80IHFsnDROHIzzLuTSi%2FJSQhf9U%2BIU6mUR7BBDJKWC000; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=7891cb3b736eef30fc25fee31da97ae8; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=7891cb3b736eef30fc25fee31da97ae8; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=7891cb3b736eef30fc25fee31da97ae8; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2e4ae9f16d8d617c312d7166b1829380; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 7iO2lBQK8VWyuXRznWFK + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 7iO2lBQK8VWyuXRznWFK + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '346' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/8670Fri, 02 May 2025 17:21:09 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/864 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=7c590017ee64c33e2bd5a7d1b6b3d57a; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=ff2K4F3aBgFbY8Dw0%2BlSwHX6aMqBPTCR86kNhUxbkFjUkm4TrC%2F7xdCeKTIf0ige57o7gjyPgmJ8IrHEviUY3P45mC%2FeOMzTF1Mdk2gIWhvd%2FsybfUjBmrmc1hYk9dtm; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=7c590017ee64c33e2bd5a7d1b6b3d57a; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=7c590017ee64c33e2bd5a7d1b6b3d57a; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=7c590017ee64c33e2bd5a7d1b6b3d57a; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ead6da9e08e54e02d601d3b96dbf865a; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - Mj2DNITMGKrFhq02m96X + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '282' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":864,"name":"[Sample] + Project Name | Ehuu (-273)","mtime":1746206468,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/[Sample] + Project Name | Ehuu (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + leia + 31 + 3 + + + user + luke + 31 + 3 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1419' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=8e2a8e3f23f5fec1d61e41156eb77a41; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=FQ%2B4hJ172K%2F5TZjtGMn6uazCHgJsvecIvVWYPA9wpJv1xb41XYFIk1pUvoxRqy3Fa5DxsuXAR8suL%2FMgdsm55PFhItUQz%2BfxSuZpz7JUjhdSSragLA6TQWGUSb2Kmg6k; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8e2a8e3f23f5fec1d61e41156eb77a41; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=8e2a8e3f23f5fec1d61e41156eb77a41; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=8e2a8e3f23f5fec1d61e41156eb77a41; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6801c7e487c8fdffdb4d57b418f28658; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - MwIp4IM9hGXZwdY8jdjd + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - MwIp4IM9hGXZwdY8jdjd + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '402' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/865 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=0c161953091a955b48b6253f05106657; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=MH7iwDNXQLidsPtQnCMFaH5ENK8EG3yD659hgETQOp2uiFjkGTUGyQdoPag31hy9u%2F%2FWidmYI7r5ZgeDr%2F9oyju%2BS3Uv4%2BbKTbDICbfce0JDuvTp58oArUaKL9i95%2B2i; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0c161953091a955b48b6253f05106657; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=0c161953091a955b48b6253f05106657; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=0c161953091a955b48b6253f05106657; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2df9add7879dbb8d1d60f9680889eb39; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - q0eAKVPSkgU7Hp0Ff75i + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '294' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":865,"name":"\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)","mtime":1746206468,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + leia + 31 + 3 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1179' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6fd921b19c43f85e55a2687ae3060fea; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=iwnZcAXxAuARlqfQ0rCHesCV0lpxxXXh5mv8qUXaJa397eYwHt10Q6Bae3qE%2F90XgCWJjD%2FdD99IilnqmZm%2BdwzgT9NhP1dnccfRoaCOR%2FX2YKN1JozNaXAPHsN6Fzaw; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6fd921b19c43f85e55a2687ae3060fea; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6fd921b19c43f85e55a2687ae3060fea; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6fd921b19c43f85e55a2687ae3060fea; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c123dbc126e7b8646c02c1216da8eab3; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 6Hmced8SqYelEc1sCmiq + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 6Hmced8SqYelEc1sCmiq + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '423' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/867 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9a9fa364376a91468f7f8051bccf97db; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=yXREkxOv680VQRMliXxmoAIvM2q3wSv8qGFiGKXXFkJzVE6JPu2DNuRmYrL%2F5eLbnQvbDVJDp4VODHf%2Byh22lV1nyIX8TOl2QrytmGw%2B1zUBmMVTZgacRiXYRh4R4E2t; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9a9fa364376a91468f7f8051bccf97db; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9a9fa364376a91468f7f8051bccf97db; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=9a9fa364376a91468f7f8051bccf97db; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ac7312e635fa659bd39d61bcdd5ddd35; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - tVtOXvJDYZDIBmhR1UWu + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '277' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":867,"name":"PUBLIC + PROJECT (-273)","mtime":1746206469,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/PUBLIC + PROJECT (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + admin + 31 + 1 + + + user + leia + 31 + 1 + + + user + luke + 31 + 1 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1660' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6d5285a836941d10f419aca19ce6dd1d; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=dZKB5wpXxk%2F3dT08ZYnv4O1wMrYsUs8MZ%2B8%2FziylbwgTA6GZrJvsYqmNFtztr9LGOEIscs3t%2BUMQsTP9N7nxHDSZZlWqiiKLpt0aTP6WdWNerMcg6kCsg0oCQUEe4Hqj; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6d5285a836941d10f419aca19ce6dd1d; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6d5285a836941d10f419aca19ce6dd1d; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6d5285a836941d10f419aca19ce6dd1d; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f8ea652a0d0c8eacdee411fcdeea632c; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - Q9dSZiKF5fdPJqMQlCNA + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - Q9dSZiKF5fdPJqMQlCNA + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '376' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/cloud/groups/OpenProject + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=2bf77890bdd087fa9f33b83123a1bddf; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=KlPKVQSOWjZaqhdhSxdJBbkKV%2BgIiX%2FfWyDA%2BRCcvzCAXe6SKFSHD6hOs8l6h1IpQJ6%2Fik5c8Bo%2BsrTuyPMTTQBoWBYUi7h%2FHWsnMAYGq0rd4KX3Yq6D8RT%2FAhCpf2kK; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2bf77890bdd087fa9f33b83123a1bddf; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=2bf77890bdd087fa9f33b83123a1bddf; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=2bf77890bdd087fa9f33b83123a1bddf; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=990bd3722f21ce9bd1c000626405ba91; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - o6HIU4fAU8ZRAfaRxrtY + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '186' + body: + encoding: UTF-8 + string: | + + + + ok + 100 + OK + + + + + + admin + anakin + leia + luke + OpenProject + + + + recorded_at: Fri, 02 May 2025 17:21:09 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '141' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:09 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=e0c4182078cef2d52203c4f8e478ff8b; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=AlmY%2FNXoqdrn0sN0Gn3Xjo8xxmbEGmIR7kiigMfGDTnLhv2Fz7lfWdWjU2YDxBE3paKqkbIU06UsqPCKknmhxblxliiNpes0bevPDRoMyU3toRDZqTGuG65We1rt9jkq; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e0c4182078cef2d52203c4f8e478ff8b; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=e0c4182078cef2d52203c4f8e478ff8b; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=e0c4182078cef2d52203c4f8e478ff8b; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=7b41f3dd6af6d085b99de95cfc967ad0; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - PmZUR5vnavypupHjgigU + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - PmZUR5vnavypupHjgigU + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '387' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/groupOpenProjectOpenProject310userOpenProjectOpenProject3131useranakinanakin3131userleialeia313userlukeluke313HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:10 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '141' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:10 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=0c9f5f27b5f48b7a15ad9fd35e764a6c; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2By%2FEaJ0LBUok5B9uBeFqicYtvi2axQt9AqS4xF8xwERTnFonfgW%2F0p91%2Fb6FAeuODwCdJUJe8qT3GQ4%2F2uzWx20FNkCUWiorLT6JFg9Z1M9Ttpx5mTxJjV1qRr8mFpQv; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0c9f5f27b5f48b7a15ad9fd35e764a6c; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=0c9f5f27b5f48b7a15ad9fd35e764a6c; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=0c9f5f27b5f48b7a15ad9fd35e764a6c; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=021ba1103fa86daf4ef3f11ce0c886ff; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - jebNb1hfR14ZBzvtrji7 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - jebNb1hfR14ZBzvtrji7 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '384' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/groupOpenProjectOpenProject310userOpenProjectOpenProject3131useranakinanakin3131userleialeia313HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:21:10 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '141' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:21:10 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a41868cb7573d096891ac8bc431d17dc; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=8GfggjsXISBekh02w98m4t5pPrU%2FS3SnTXOf7PaNzem8UchXxRayOxgCUesl0Sx5c%2BLEJ4S5C5VX6riQZ%2Btb1Dv6buSKsUs7AfL3IBXw2INk%2FZ5WtGl5K2xUvRAwEeaO; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a41868cb7573d096891ac8bc431d17dc; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a41868cb7573d096891ac8bc431d17dc; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a41868cb7573d096891ac8bc431d17dc; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=dbac90e14069a62fba94c0e6a603bfdb; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - eDfRki40dTAYs6Pugw76 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - eDfRki40dTAYs6Pugw76 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '270' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)/HTTP/1.1 404 Not Found + recorded_at: Fri, 02 May 2025 17:21:10 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:21:10 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d86febe7c3bdb3e94ef1e003dc15b261; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=JM7A1KdcPlhD88X49t6WzSj5JzEJC4In%2FqfoCnB6FgXOGIk3XMtj%2FWhu398Q%2FU%2Bp1aIOR%2FioJlz2IbVCd9PAxD%2FIqsB3IudQ3PFhD15DgbJDoLXC7By8TAcfLxOAiWNQ; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d86febe7c3bdb3e94ef1e003dc15b261; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d86febe7c3bdb3e94ef1e003dc15b261; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d86febe7c3bdb3e94ef1e003dc15b261; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8383f788c1cd394ae31758c3eab972db; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - cRzj1ej6E9SHxQMiQ9uL + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - cRzj1ej6E9SHxQMiQ9uL + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:21:10 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:21:10 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=bfb8d19c857a55c879f055371ffac2e2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=QJcnMYn7y6fIZrEO0wwKqoem7ecCLZaDJZAIT1ZiVvRqQ5Vrmd%2FCOSTIt1x26sGhTEby9e29aNT0XNxxyKHx33SmUMnt4486LPwU5xCOJ49fWF93FLMLKDjN9AY6UGdI; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=bfb8d19c857a55c879f055371ffac2e2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=bfb8d19c857a55c879f055371ffac2e2; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=bfb8d19c857a55c879f055371ffac2e2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=988ffa708d59a96ac9fc2da85fbb31c8; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - wK9xbIU37hG39zQ9e1N8 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - wK9xbIU37hG39zQ9e1N8 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:21:10 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:21:10 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=fb3a6d6fbcda46668c424862b4edc020; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=gTA7x9MTYh1wKbVV2LzLlQwv3H%2BmHSFy4w2G225UGEb%2FloneYptZRor9DDFmacgM5J7d3aLpOkGeZZvwwnP9qfjbOX%2FAqHkbssD0FHi65Ogv7kD4vOQXthZa1ErvY8eu; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fb3a6d6fbcda46668c424862b4edc020; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=fb3a6d6fbcda46668c424862b4edc020; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=fb3a6d6fbcda46668c424862b4edc020; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=af140ba2dd51c690f8dce24b7a6e33c1; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - Jbew6tqob2K0U6XlMlAE + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - Jbew6tqob2K0U6XlMlAE + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:21:10 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:21:10 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=fbd7413dd7e4d9ff2e6806aa5917855c; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=uvfhQfMMpvArYyWzLwz82XWzpNA2iKKkL8ILX0PVLSlS4mpIM7mPMyFNPTszgbhUOzm1OjQzWnc%2F09M6%2BH1%2BlZAnp8ZTxjIaSF63l0Iu9EwQDzxUyx20KqKf3kzdjhaH; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fbd7413dd7e4d9ff2e6806aa5917855c; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=fbd7413dd7e4d9ff2e6806aa5917855c; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=fbd7413dd7e4d9ff2e6806aa5917855c; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=31dae4f8548eb4bed3e59b70fb2f03cf; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - TtDPjOFmNEDft3GfreL0 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - TtDPjOFmNEDft3GfreL0 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:21:10 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_admin_access.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_admin_access.yml new file mode 100644 index 00000000000..faf78046b9c --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_admin_access.yml @@ -0,0 +1,1886 @@ +--- +http_interactions: +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:39:22 GMT + Oc-Fileid: + - '00000868ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6a133c6b9099b69120440fdfae2eea61; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=mdvN87buJhoNsvUhBb8%2F%2BVUilri%2Bf9GMfbGX4KuRk5ZSrCNYm%2FxzjU7%2BnwdGRiPAa%2F5MjeHL6x8Xj%2FXStbOcMcDvzCCZRM71C8QQuiFpikm%2BhM2BCC9gVKFsJqY9%2Ff2M; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6a133c6b9099b69120440fdfae2eea61; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6a133c6b9099b69120440fdfae2eea61; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6a133c6b9099b69120440fdfae2eea61; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3ffa929f4189119ca80f456fbf02f715; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - eQfLxxwSBDhjwWr0YCMc + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - eQfLxxwSBDhjwWr0YCMc + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:39:22 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:22 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=3e6f76971d86d821da0c748e29108add; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=HmTuVKBfp6Zd%2BK7qnW9SC8MTsUUIxjZYzRvuzGwVUEZUKpMoV7KoUzvXO9ei3SFvkBnh8CKgv2h%2FZqaMWB0TH3oSQ2OilDI7xj1loQ%2F8sXv625uoAxECwGoJmoFcyo5G; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3e6f76971d86d821da0c748e29108add; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=3e6f76971d86d821da0c748e29108add; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=3e6f76971d86d821da0c748e29108add; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0f2f3e682559f7c10391d40b06a3230d; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 5cqk0f4gZWWZG3juixjW + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 5cqk0f4gZWWZG3juixjW + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '361' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/8680Fri, 02 May 2025 17:39:22 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:22 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:39:22 GMT + Oc-Fileid: + - '00000869ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=3f3151ef5f3643375e620eb71402ae14; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=4N63YCsAMyCKlTq2R6vCJbA18pkL0UUe3rAbzhXdjZR9u1waFridP2iG32QxhxCGgMrTrFvZXhwKGSpPLCAihJWKg%2FJbuPGPmKTkVmVfTepVuqgG65QqXLRPunvlFVVx; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3f3151ef5f3643375e620eb71402ae14; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=3f3151ef5f3643375e620eb71402ae14; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=3f3151ef5f3643375e620eb71402ae14; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c0b2d2ee7fe4eaf777b9e2281863fcfe; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - n5ZHtjOHiDXNDivolFJp + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - n5ZHtjOHiDXNDivolFJp + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:39:22 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:22 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=f86b9cd27d94eac2545684e311366fae; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=0se%2F73pUdu40jJexAAiWH9gEj0vcVRoj9izSzkwXquJo7cuwU5AaRcsQEKX8ZYyd53jh8Bp8JnqcEpcANAHkl3xAVRNpddCGBF3KZHrTmRtUVn1HLAp%2BWsyPUZmu4q%2BV; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f86b9cd27d94eac2545684e311366fae; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=f86b9cd27d94eac2545684e311366fae; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=f86b9cd27d94eac2545684e311366fae; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0e1430a7a4ed1de5bb604cef6533d078; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - CoFYLzpqfM6gB6GOxrut + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - CoFYLzpqfM6gB6GOxrut + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '367' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/8690Fri, 02 May 2025 17:39:22 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:22 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:39:22 GMT + Oc-Fileid: + - '00000870ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=4d277d9878b27cf048d72097de7c1721; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2BJpnK6v4vfyrweUWw6OXSMIM9xbJlwSSIChqFgHHLRkehvT2arOJA237PJ3lkM1TNTp%2BdbM9ODe9C7FRtzQBcJF%2BtBHM%2FMBsDUfi2xEdhy3s6kg4Yu6Em11E0lfnrlAK; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=4d277d9878b27cf048d72097de7c1721; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=4d277d9878b27cf048d72097de7c1721; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=4d277d9878b27cf048d72097de7c1721; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=254f8a20762cafff454498d12692e93c; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - l05cZNLfrpBiYnFlOUhv + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - l05cZNLfrpBiYnFlOUhv + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:39:22 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:22 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=e80e4e543d817c330b6431c1335c6028; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=7psH7y2eg5fvCtoCX7Us0AZgzimH8ALYfb%2F0UFHuIcOGzFyDvJaEt%2BtYoxpzjHMKnLIDR79WFf7sB1HOQiqoszsjqKKh8mmyP9s7XtNcgytHB7REFylOtmqz0S9%2BedOf; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e80e4e543d817c330b6431c1335c6028; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=e80e4e543d817c330b6431c1335c6028; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=e80e4e543d817c330b6431c1335c6028; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=45d10558054f83135cdcf408d3ef2fd0; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - ls7W6Mwl9gglfWAQwhNa + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - ls7W6Mwl9gglfWAQwhNa + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '361' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)/8700Fri, 02 May 2025 17:39:22 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Oc-Fileid: + - '00000871ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=487b61d2e7a9c5afdaa66232d66e3181; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=18vcqJ%2FLMaJziYkFWbz7Jvl82edKBMBbEPl2c62%2FLQ7y%2BSssjip0vCl9AtR6UnUfihhFloMI6s0C1P2OrdrmD2uyUzM3%2BhEpp6iXA4kYfztnh%2FwLv%2FbCJVTLQINJkfZU; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=487b61d2e7a9c5afdaa66232d66e3181; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=487b61d2e7a9c5afdaa66232d66e3181; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=487b61d2e7a9c5afdaa66232d66e3181; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6584c08438a0cfcfcef9b8858b3c93e5; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - XmWkhc9xMLqOooxY7dcs + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - XmWkhc9xMLqOooxY7dcs + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=dc0aaf089b798e36c523a774597214c1; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=QlJeHzu192nU0HYl1tQPrgQc6eF6%2BbdUaYRLMUOy4XXnatDPd3haKvcKXHPLAvkwQcp3%2Bn0hQOALAEna1VOnsGQaCBegwGVtV2qF753T9H0LZktItO2PjgtMm7p1QBi4; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=dc0aaf089b798e36c523a774597214c1; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=dc0aaf089b798e36c523a774597214c1; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=dc0aaf089b798e36c523a774597214c1; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1e4b9519f16e97a8597a86d48d89c6da; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - I7GiBz8dmEG7f58CCdVe + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - I7GiBz8dmEG7f58CCdVe + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '346' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/8710Fri, 02 May 2025 17:39:23 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/868 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=97d524aecfa213fc9d1fc825e1dca8b6; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=L1MwACG0tsOHJMdYLL5QpxJSGHaAqSsf2F6lZmNAKP6WJ7g15G%2BQq4e2XZOcnZAQaLXHt1dqKJ69PweWz9fBH%2BRaJ2KxNnznUIjkrABE3tzSP0E9L2K9a2NvFtPsz1Uf; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=97d524aecfa213fc9d1fc825e1dca8b6; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=97d524aecfa213fc9d1fc825e1dca8b6; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=97d524aecfa213fc9d1fc825e1dca8b6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3ed7e59d1f432a4f584f954de350a2bc; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - M7snYAvWOdNpzMvfFf4h + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '282' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":868,"name":"[Sample] + Project Name | Ehuu (-273)","mtime":1746207562,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/[Sample] + Project Name | Ehuu (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + leia + 31 + 3 + + + user + luke + 31 + 3 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1419' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=40ee65ce041574eaead67d0204e51a16; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=oCD7yIZt3QINqtRpUVvBK8QtIJLa8fkf4mNqun8KU0iqhGqouI5kO2ZpsXnL4F35hKC0MM6A3wmHtwTmyi6dAGmpHPJVIdET%2FuYvV%2FMpHJlG%2FqQKTlztYfbLlcG0RxzT; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=40ee65ce041574eaead67d0204e51a16; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=40ee65ce041574eaead67d0204e51a16; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=40ee65ce041574eaead67d0204e51a16; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6903b27704e969a30fb2729776f2ee70; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - eSXGxK39DVhHm0z0M9ba + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - eSXGxK39DVhHm0z0M9ba + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '402' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/869 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=89e24728566e4796c292202e9dd80524; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=54GMssHPmPunaLujRQCu5JX7CMp482ThKp4QBN6kPzoWpe%2BeAGUu%2FPJfrAXeG0kDGHhH%2BIYudxaRddF8SA6y%2FJckbFlV6TW0tQCE%2B0pukqUdaP5VRnO2MFbcoylMLscI; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=89e24728566e4796c292202e9dd80524; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=89e24728566e4796c292202e9dd80524; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=89e24728566e4796c292202e9dd80524; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0c843df94651e57af45b809fe97c3c03; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - tpiOYUwF4g1PvEzHh0KU + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '294' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":869,"name":"\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)","mtime":1746207562,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + leia + 31 + 3 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1179' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=59a3c36604ac6cbacb8279b3b10e90c0; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=4PqR4ClNz9TTMkn%2BDTOhXxroykXasGONnk7iPuEvV6mCYKlpiz%2Bji7MNYjeFMSjc9J7kqpgBQLBciBr%2F1vuMxWgk6yhquVibX8n8LsNbkKaTQRFCGJgtioCBnGcgdWqz; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=59a3c36604ac6cbacb8279b3b10e90c0; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=59a3c36604ac6cbacb8279b3b10e90c0; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=59a3c36604ac6cbacb8279b3b10e90c0; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=be6af12cb57761d7e7536513a9e80842; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - ywodqupt8j1sSDbcSrPh + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - ywodqupt8j1sSDbcSrPh + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '423' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/871 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6a05206e1e8cc253be0badc71c903812; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=85jVeC4Ivu39MzdaJ8NwRLJGsVPJiAwhyEKShbcyeAe277Ith6tVwhH9X1nMXbqhSelPmbGii3PN30WWuhpX5L8D5PvwQxsEYqNQiflQcw7ysibTVIrKo8R5DIGYbYBz; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6a05206e1e8cc253be0badc71c903812; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6a05206e1e8cc253be0badc71c903812; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=6a05206e1e8cc253be0badc71c903812; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=421fb69f26c672e649279cfdf72d17fd; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - Wgkm1qfirxMPbb0UjRjj + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '276' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":871,"name":"PUBLIC + PROJECT (-273)","mtime":1746207563,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/PUBLIC + PROJECT (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + admin + 31 + 1 + + + user + leia + 31 + 1 + + + user + luke + 31 + 1 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1660' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=065bfe70879dd10fc3fc0b8ec462de6f; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=hW00zv3KYiBelEUBLpiVaZOzRN%2FmJGqkqpKjGD53Kx99joY7YNsgX31brZedQYeMK9yAxrQGG7nEJ8m0hTeos3Eymb598mMO%2Bf3WFPDpSH0kSPVt69mSws2mtFzuI%2BT4; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=065bfe70879dd10fc3fc0b8ec462de6f; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=065bfe70879dd10fc3fc0b8ec462de6f; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=065bfe70879dd10fc3fc0b8ec462de6f; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2a1473fa14ed4f06a3babae4e4fd96f7; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 1IMOxTKM6EvKB76lK4d0 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 1IMOxTKM6EvKB76lK4d0 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '376' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/cloud/groups/OpenProject + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=5d183a59aae7adea43e86a6a808cad79; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=a6rkqosxqA2GMXB1JPhvtLO%2BzuCty4LMnN3ds1GkITBL24Qec2MIxB85vCfJMumgqUwVF4gwcFEJAawodUM4aOyXShYjVfF6URJ%2FAUroMWaEkf4ohEq%2BD64fyNEnu0Fd; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=5d183a59aae7adea43e86a6a808cad79; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=5d183a59aae7adea43e86a6a808cad79; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=5d183a59aae7adea43e86a6a808cad79; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a82354446b2fc5c4a51f8cae6c364fed; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - cerGdataXG8NNfoS1i2w + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '186' + body: + encoding: UTF-8 + string: | + + + + ok + 100 + OK + + + + + + admin + anakin + leia + luke + OpenProject + + + + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '141' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=8b6335c91bb9a487dfcba4c02958f7d5; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Eh6YDyA8gOdfoLVvd96SJbzq%2BF4gmQcnEzaO%2FO%2F7Gm6dWd5IFLOo1GyEzbImW9V3xV%2Bq2e0YmrRSA9djYcs2JIyZ0qJXgFZtL%2BHK9lA3y0UCbkntK2cQR%2FlNK7eySP22; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8b6335c91bb9a487dfcba4c02958f7d5; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=8b6335c91bb9a487dfcba4c02958f7d5; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=8b6335c91bb9a487dfcba4c02958f7d5; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=161ac5ff68f30a19c4949f859480e326; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 7Jl5QIHC28AP4wIaI97l + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 7Jl5QIHC28AP4wIaI97l + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '387' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/groupOpenProjectOpenProject310userOpenProjectOpenProject3131useranakinanakin3131userleialeia313userlukeluke313HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:23 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '141' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:23 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=3d72249e9d729199bb73545440b72c0c; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=W6B1cwkZtJGsdNnESuujzEh3TMLGFoRXbs2dqVQBPh5EIQyWsjkyIzDAcnhXK6c2g4LSLquD6SClBdOP64CzxVEsR5CClROUI39EtyFu9fyEJ%2FsvGr90Pa6lS89PHjxP; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3d72249e9d729199bb73545440b72c0c; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=3d72249e9d729199bb73545440b72c0c; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=3d72249e9d729199bb73545440b72c0c; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=07e3a2a32dab9e0c84cbc8f0a15fe0c1; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - z0oIGWXS79XzoOpPvnpJ + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - z0oIGWXS79XzoOpPvnpJ + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '384' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/groupOpenProjectOpenProject310userOpenProjectOpenProject3131useranakinanakin3131userleialeia313HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:24 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '141' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:39:24 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1173cdfcfa4f997bdac6278229cb899f; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=uJnuyaHHf0sAID0q4QiyUGS77lSmiaPE7nRNLkEMHk44XXS33mfrnwYRqtDHkyomXh1yp22opDWmZOF65Z7iQH%2FhVcpELm2rl7iYKaSvScAfM7lnGnqWh2K4bFMngFVV; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1173cdfcfa4f997bdac6278229cb899f; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1173cdfcfa4f997bdac6278229cb899f; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=1173cdfcfa4f997bdac6278229cb899f; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e4b8ea48b07e8612bdf475a16aad4534; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - lsanVACE0yKBhXphgDk7 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - lsanVACE0yKBhXphgDk7 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '383' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/groupOpenProjectOpenProject310userOpenProjectOpenProject3131useranakinanakin3131useradminadmin311userleialeia311userlukeluke311HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:39:24 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:39:24 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=c0f72de034d44645ae22f1bb289f3008; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=16c1CT8kcFV7Z9QCFMuVfgsnmw7c6Htm%2BwDEDSINaE%2BLg3cQidYdVzNPVG4jXD%2BeVmZNiyvC2yU14xZ5FjSdU4r1NnswES7Ow0SXaJOKt4z5g%2FLEtOoVnd9BSGoryV4t; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c0f72de034d44645ae22f1bb289f3008; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=c0f72de034d44645ae22f1bb289f3008; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=c0f72de034d44645ae22f1bb289f3008; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=09bcd578e0a2fd9268ded3c721ee17e9; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - EYHPj9vHo9uvr4cB8ByC + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - EYHPj9vHo9uvr4cB8ByC + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:39:24 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:39:24 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=2e11888459670b55fc385c07936bfc1a; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=2SJjFCqeeQhvVisOVUBPI6J9HdOoSXXoZ5pXubxFKBBfgTCDWrW0Gb2HaUh3WGF2qONqQXUwhzTGq4opVxvUJRrV5T%2F%2FkIiQ46ByYhoXf4WORGfCc5edJNFU3GZuSjrp; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2e11888459670b55fc385c07936bfc1a; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=2e11888459670b55fc385c07936bfc1a; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=2e11888459670b55fc385c07936bfc1a; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=14e6b106ebd867a110d301e3a3922035; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - f2dAl88noGVUrwGbcLoP + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - f2dAl88noGVUrwGbcLoP + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:39:24 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:39:24 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=29406115b19b89d24359d3005334a30f; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=k%2BFp1yY35%2B7WYnbZbaZFF%2B%2FLh7Q7nHja1UY%2BTfNBPR6R5re9YY9X1son4DPxAzMDdog2DTn7CkFAS%2BJRXtddQC3%2FZtTttLUBrIcvgUiNBLtImA66c6U2UOpc3e7e%2BcME; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=29406115b19b89d24359d3005334a30f; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=29406115b19b89d24359d3005334a30f; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=29406115b19b89d24359d3005334a30f; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d7bdfbad36aebd4ad5694bc8da7c7d19; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - OiN8lHnuLGyuabMzL1Sf + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - OiN8lHnuLGyuabMzL1Sf + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:39:24 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:39:24 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9e0197201a5937c1142ac4424f64fe70; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=BEshex4V4mNDcv3uTcmnv01ytEpgN1NnGFg72nBYVN0LZJbe%2FxR%2F2InnzoDZgE3qcv%2FnSdYrA8Ayl2Vqu4SnOay50QOuqXzSm%2Bf8BwpeTG33Sy7WSO0pJgRmVH4ILemN; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9e0197201a5937c1142ac4424f64fe70; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9e0197201a5937c1142ac4424f64fe70; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=9e0197201a5937c1142ac4424f64fe70; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ed329bffbb6ed50b82afdfa18e38e334; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - dMdkxFVhfljDflHI4qC6 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - dMdkxFVhfljDflHI4qC6 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:39:24 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_group_users.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_group_users.yml new file mode 100644 index 00000000000..5240b683910 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_group_users.yml @@ -0,0 +1,1979 @@ +--- +http_interactions: +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:40:01 GMT + Oc-Fileid: + - '00000872ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=335b7285d8e9675431b6531ac878ba9d; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=WJwW65is3OQzUR%2BawqRokYbnFbVBXlSjxG3QiS5NJeK3q109hc8NIYDzNdR8BY6nuIWjupLA489B9CyuGd3m74PQx66tw%2FzM%2FznCZ3Bhf12Ls6ZUsb199n0SAFD98b9z; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=335b7285d8e9675431b6531ac878ba9d; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=335b7285d8e9675431b6531ac878ba9d; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=335b7285d8e9675431b6531ac878ba9d; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=238df4b9c5a097657d3e31a5d2201eec; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - XlNNheDKrp5dxNHqdgQz + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - XlNNheDKrp5dxNHqdgQz + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:40:01 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:01 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=afccae5be17986fe8c4de2f2129df8ab; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=HZc3Ou9ToSjJKTn2us3XFzrUH3SuO4OXs45lb5ebusZi%2F8wpbOkKqQ0DqD0KERI%2FRQB8KWj7ImcF9Xc%2BYSOcaeSDB5tvbao5sKw24Eu8lo4wdzvE92Rs0CNrY%2FN2LH2n; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=afccae5be17986fe8c4de2f2129df8ab; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=afccae5be17986fe8c4de2f2129df8ab; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=afccae5be17986fe8c4de2f2129df8ab; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=86f9263d7235acceac0b88de2c461a03; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - PTCjWIwx6dFJnNWT2kME + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - PTCjWIwx6dFJnNWT2kME + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '360' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/8720Fri, 02 May 2025 17:40:01 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:40:01 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:40:01 GMT + Oc-Fileid: + - '00000873ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9c0c8b2bc4b07fd8c65462b0b2d89279; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=KTSoTiOEJNIinrFDPhBWcwD6jqcXtu3mG83beC3lq37L31m0KJyWFNXUl0ho5tGt6o8TMvQqBjwQlKwrfhjHRRJ3MzROLVdl7MggRMzqrzzGLqEmmX1cPEcfv5vIsixu; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9c0c8b2bc4b07fd8c65462b0b2d89279; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9c0c8b2bc4b07fd8c65462b0b2d89279; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=9c0c8b2bc4b07fd8c65462b0b2d89279; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=816861aa0c5861446959288a09261483; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - CgBGojxzQuWPj9gadLND + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - CgBGojxzQuWPj9gadLND + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:40:01 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:01 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a88a62143a5defe1a7cb655e96635bff; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=FC4uQ7Mvz7%2BbDAf6l9te3%2BNnpWExj6uIfChbtRaZcvQTzWEOoJEuKnCYVKGmBzXhsTrqj7QjCD5W0Ee7OAWXosUV6l4XQEHiocUy%2B9JgVNVYOxWLuA6wEXS4VgDZPcNf; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a88a62143a5defe1a7cb655e96635bff; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a88a62143a5defe1a7cb655e96635bff; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a88a62143a5defe1a7cb655e96635bff; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=7b60ce707c885a7c86d017600beb9f4b; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - EridxRbNHxR59ghE7GNO + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - EridxRbNHxR59ghE7GNO + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '367' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/8730Fri, 02 May 2025 17:40:01 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:40:01 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:40:01 GMT + Oc-Fileid: + - '00000874ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a4eb4d3bd83c5d1cfc7ab3c9baa7d025; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=eh5hPOPmRmXR5Zclyr02CoQ8Dsltf9%2B2kqBk0mqNFyobDDdjXeVwKmSsKvoA6aQetXM%2BNosBandp7fn9KDKYjhvIiRsZuN4Tcp2JxJlPpgafngSEteKPsKHf4x%2F9QKz%2F; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a4eb4d3bd83c5d1cfc7ab3c9baa7d025; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a4eb4d3bd83c5d1cfc7ab3c9baa7d025; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a4eb4d3bd83c5d1cfc7ab3c9baa7d025; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=5f60499006d4fc08cc040158e0a02912; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - mLluOmnxtsgrEIBcfpoL + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - mLluOmnxtsgrEIBcfpoL + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:40:01 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:01 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=fc52bba172c779228242fdeb96c888ad; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Zl7L2tUaqDNcZMX%2FsHOngW4S8zV3rDm5P6%2B7BEX7oN2iEI3xv5S4hSjTsCvH8kg3Uz1rx1aDEf3VSnGjTFgqFTq3HWQU4dPJo6QwyBqE1qGZgNCM5XGLvORRtWTfYlte; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fc52bba172c779228242fdeb96c888ad; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=fc52bba172c779228242fdeb96c888ad; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=fc52bba172c779228242fdeb96c888ad; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e96d0d955d3312b013c9b81d5dc46d6e; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - rN2RD9n1kwpu2tiOohsN + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - rN2RD9n1kwpu2tiOohsN + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '361' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)/8740Fri, 02 May 2025 17:40:01 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Oc-Fileid: + - '00000875ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=2a806fd66c90d545794c57222cabc848; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=l5tS9x9SGr%2FcBkMB%2BfNvetOpELTBesyraUFO%2FkM2peN5Qcdy8eu%2BCegFueoTljO%2FUKvotmz3yIMpIx9HWMzm5ChN0iihOSx00OWEZbpdzr8LL75rRYwOkoGhfj1r65Y0; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=2a806fd66c90d545794c57222cabc848; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=2a806fd66c90d545794c57222cabc848; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=2a806fd66c90d545794c57222cabc848; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8448b7aac2eb621c0f1f6d218dba303c; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 5Jswu09GbySd3AzvtX2l + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 5Jswu09GbySd3AzvtX2l + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=116000ffaac27fa0ffa698f84a4655bc; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=7wPsK%2FRYnN4M3zpNgFT0lcIi4pUDilePXVLCs6tZ%2B6M3TehR5UpcDREtrZQkcdbeH6iCrGbO7fcsLvAcTmjziJCSihIe0a1QQk70aTftCvMwTW%2FN2bQMNLvlKuTN%2Bbmo; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=116000ffaac27fa0ffa698f84a4655bc; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=116000ffaac27fa0ffa698f84a4655bc; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=116000ffaac27fa0ffa698f84a4655bc; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3a6feeb5bf41824e6100b85261643065; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - h4NPMoSmfeSGXBNevUVt + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - h4NPMoSmfeSGXBNevUVt + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '345' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/8750Fri, 02 May 2025 17:40:02 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/872 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a9def1b55df78b77a86c9ed0c58acaab; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=rFPQcQYNFZB2iX21jK4lOlcRbEEJVPqs3b0uBO61P6rgn6kiqttOn0iu%2BPQ1hlUWEm47p1aLAOjAxZdk4YeUk5N4T0C5vDHPWvY%2FosELl5l%2BG%2F6BrD8LBv%2FAzv1aeBDS; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a9def1b55df78b77a86c9ed0c58acaab; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a9def1b55df78b77a86c9ed0c58acaab; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a9def1b55df78b77a86c9ed0c58acaab; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=09f08b3581b76e35cdef242cfb012131; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - E3dgpFC0KMaOqixbugDu + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '281' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":872,"name":"[Sample] + Project Name | Ehuu (-273)","mtime":1746207601,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/[Sample] + Project Name | Ehuu (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + leia + 31 + 3 + + + user + luke + 31 + 3 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1419' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=e10ba843ce48b3a6ca57a6524d4aab64; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Q0E6FHmnIlXUTT5gXtCLZPsQJkNV0t2nRukcx8xjE3yj%2BFQ1XwSn8176UqKcnln%2B8NjY5myAlM2bot055L4rWgCzNBYELwnnMPq%2BPGZnmjJyK1vvnckYyQ2wlhpfZalO; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e10ba843ce48b3a6ca57a6524d4aab64; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=e10ba843ce48b3a6ca57a6524d4aab64; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=e10ba843ce48b3a6ca57a6524d4aab64; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=18f6b6783907728ccf170b81e8efe2c2; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - fXroQyDuKOmB3xI5qgXN + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - fXroQyDuKOmB3xI5qgXN + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '402' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/873 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=3c21c5983959815eafeceb8dd590a93b; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=nluTRb61gpueDos10Iqnb1qdba5PnuWksm9vaA%2F0ddjy59CRAlxlFR3FIAUaNiIAD%2Fh9fO09odCt%2FpW81wNb9UZa9YypoQvxFCRN4zrq7Q55%2BDRzTZrPuTyLCnviiw9Q; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3c21c5983959815eafeceb8dd590a93b; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=3c21c5983959815eafeceb8dd590a93b; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=3c21c5983959815eafeceb8dd590a93b; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=335cf43fc7e3d83f4cce6f5cadc14936; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - k3whbm2my88gI0q8aAIG + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '293' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":873,"name":"\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)","mtime":1746207601,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + leia + 31 + 3 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1179' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=f35157808d97cd1b36896e0db6570f41; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Iy%2Fsuku177XlB2ZCPXl6ZTQEhm2k5y4HFc75QKv2AjKWcYXozrTEPxicANYbmVc6aCVW9QYe59IxOCHAh1gvoBFWgdEafRisN6GLFKg%2FrD3LpBeIddCcKwUhDhDRNjam; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f35157808d97cd1b36896e0db6570f41; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=f35157808d97cd1b36896e0db6570f41; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=f35157808d97cd1b36896e0db6570f41; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=15dbd9563859ee99ee2935579239e5c8; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - DBnasOkA8PJR3T3a5PTd + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - DBnasOkA8PJR3T3a5PTd + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '423' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/875 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=31a39801fa545e44cdc348f3bd467984; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=AtSpADkXik85W06lQoJGXhO6S%2BS8j%2B5Cen%2FZ%2Fs4b0v73e%2Fm6Qnm%2FqLGxmfsMT1a%2FTQpyyzHQ5Apkf7RmRuCJMvVoaNQcd1qig%2BxwLsPdzL%2FQ2aGCK9Sxsf3E0xuFSiJX; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=31a39801fa545e44cdc348f3bd467984; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=31a39801fa545e44cdc348f3bd467984; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=31a39801fa545e44cdc348f3bd467984; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=fab621d8a37f06cae2b9436eaaefb51d; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - I3zDflbS6K2Er2twGkwo + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '276' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":875,"name":"PUBLIC + PROJECT (-273)","mtime":1746207602,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/PUBLIC + PROJECT (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + admin + 31 + 1 + + + user + leia + 31 + 1 + + + user + luke + 31 + 1 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1660' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9227e9e2df662d0e2330b4dfaf918586; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=eoetSPAkOQTXkZNDbfCPZNEf3GpC6%2Fiy0GDu5gIojkwSvTkYi6z5PUvHQ6LkLNzZ6Z79JUx%2Bl%2BObBdlKKkpd2G%2Fn%2BJPuP8A4tdc82rGdsiEjSdYiDCqL4CK4QhX1uHUT; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9227e9e2df662d0e2330b4dfaf918586; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9227e9e2df662d0e2330b4dfaf918586; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=9227e9e2df662d0e2330b4dfaf918586; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=85e5645be625008562715cdebc4744a4; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - quOWKIdnEN4BjqcJfrcV + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - quOWKIdnEN4BjqcJfrcV + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '376' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/cloud/groups/OpenProject + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=0788cbb0723d371a7a6f0502b13d46f3; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=i7qnJ6kds%2F1PdNKBzRiyBq%2FRz8hoFXTu6bf1QAU%2FR%2B61A9cKPFi8UniBK3L6SbbIyj8b6ep1nKHjP828pi9MRxLxqvLi6YHG8q0ozgFDIic8JkHCtUqAStoSdjSZtYeW; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0788cbb0723d371a7a6f0502b13d46f3; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=0788cbb0723d371a7a6f0502b13d46f3; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=0788cbb0723d371a7a6f0502b13d46f3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c9c1d9ce545ce0598e0806ef7579e455; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - cltUa7R7I32YcGC8aKfh + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '186' + body: + encoding: UTF-8 + string: | + + + + ok + 100 + OK + + + + + + admin + anakin + leia + luke + OpenProject + + + + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/cloud/groups/OpenProject + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:02 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=29924f2397ce90badc967ed0691eda34; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=QjPRRlUYVIAL4iGKVe%2F%2F3vDgyFawdJPG45jWFxmxWnJ16PlF40hIztRtSMbg7SaUQy1RZkA0fO5dZ%2Fr%2B9jyhM8pDGBy2ATrH2td%2BOE7n0Xi%2BN7iKc1PsICTxBpMVMMs7; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=29924f2397ce90badc967ed0691eda34; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=29924f2397ce90badc967ed0691eda34; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=29924f2397ce90badc967ed0691eda34; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=db65489ce0be61ef4525fc7278da68d2; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - t6BbhQvPyQvOnTD2qs53 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '186' + body: + encoding: UTF-8 + string: | + + + + ok + 100 + OK + + + + + + admin + anakin + leia + luke + OpenProject + + + + recorded_at: Fri, 02 May 2025 17:40:02 GMT +- request: + method: delete + uri: https://nextcloud.local/ocs/v1.php/cloud/users/anakin/groups?groupid=OpenProject + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:03 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=866ba44b46868f8f3fc056761129002c; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=msHHWhmBf%2BqzpqwsLpu7MBA%2BnhgJdacanBr6%2Fc%2FKxpyFYfi7lYiMcyoEWGvF87AYbr1%2BUcjCe8XrMrzrf3CcmZ7I9VyOkP%2BaTKT%2BgGcXdRWeAyw77%2Fe1Hwz1Rb6ruxfx; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=866ba44b46868f8f3fc056761129002c; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=866ba44b46868f8f3fc056761129002c; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=866ba44b46868f8f3fc056761129002c; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=69cff6e0fb600812dfdacfc5b765c063; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - BmZsS4GRvhM0hRaAD8Vw + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '183' + body: + encoding: UTF-8 + string: | + + + + failure + 105 + Not viable to remove user from the last group you are sub-admin of + + + + + + recorded_at: Fri, 02 May 2025 17:40:03 GMT +- request: + method: delete + uri: https://nextcloud.local/ocs/v1.php/cloud/users/luke/groups?groupid=OpenProject + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:03 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d7b29aaf30168f13c3e8c10d4ba8aa8a; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=MjemrncZldhBR%2F7FcimqvSPfNGyAN%2FbyrjJyzl8uGnLXj%2FGKAm3cL341MQVhyq7gICRK8vxtfCQFPQishylSwgJ2s%2FKIx7T5TVL6nwVg5An3dY%2FnUvFG8eS%2FDh7Y3cRS; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d7b29aaf30168f13c3e8c10d4ba8aa8a; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d7b29aaf30168f13c3e8c10d4ba8aa8a; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d7b29aaf30168f13c3e8c10d4ba8aa8a; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=14b32438b97370d94e6600eb55deceaa; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 77w3L8oYtxHxEnx1ceqv + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '183' + body: + encoding: UTF-8 + string: | + + + + failure + 105 + Not viable to remove user from the last group you are sub-admin of + + + + + + recorded_at: Fri, 02 May 2025 17:40:03 GMT +- request: + method: delete + uri: https://nextcloud.local/ocs/v1.php/cloud/users/leia/groups?groupid=OpenProject + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:40:03 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=126a3c54bd22ed92efb8fa51e03796d3; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=0A%2FpPfo5JDTdCc52MivD4vErAm5%2Fn%2Fo7S0llF42tU5QPS42SXf08JS6RHzb2icSDaKNW%2B%2FsKM9FhQRoEdrsYWydkwP%2BVs41YqkKuLsWDU44NsK4TeEnRbchB71DC8oqM; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=126a3c54bd22ed92efb8fa51e03796d3; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=126a3c54bd22ed92efb8fa51e03796d3; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=126a3c54bd22ed92efb8fa51e03796d3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f0030c1198286a31703716ee0ee4a180; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - ainuxrm9MvxtV7LX29GS + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '183' + body: + encoding: UTF-8 + string: | + + + + failure + 105 + Not viable to remove user from the last group you are sub-admin of + + + + + + recorded_at: Fri, 02 May 2025 17:40:03 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:40:03 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=53d84a7b4283c89466702ebcddccf931; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=MpgG0Mt51zYv0YkPyxEdRx59SP5nH3KnAUii9oPkkqpy%2F1SABDfSWl4%2Fb3888BDpJU6LzY%2FPqKtbuo6Ex0SWXmg9vINNnU3s0i22JQ8V9jmaDB1b7EureTWCa2MWxtAb; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=53d84a7b4283c89466702ebcddccf931; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=53d84a7b4283c89466702ebcddccf931; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=53d84a7b4283c89466702ebcddccf931; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=33b95d6f9d94219273378db1a23595f1; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - sKjDityExYHiYFXM7O9n + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - sKjDityExYHiYFXM7O9n + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:40:03 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:40:03 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a1713490f5e5164840fa293a48c344a8; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=zv57DPeu2xcc1%2BJoGDp5b03C%2BTBtDFb6tKW4Jo5%2BXfxFanBLomxKEc91WHRmZF60QJIX18zk7%2BIif74Oc%2FQLnzIv2k%2Fx7Dt7gD6KYiGBCropax0UT6nbQbnuIEuNDpVb; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a1713490f5e5164840fa293a48c344a8; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a1713490f5e5164840fa293a48c344a8; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a1713490f5e5164840fa293a48c344a8; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0a9d90bdc879db3e56987cb5d6c56356; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - CoR7JvDuoezSON2zcrZV + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - CoR7JvDuoezSON2zcrZV + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:40:03 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:40:03 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a25683f8f46a51ff822c286a8ce860a1; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=Sj5IPfdpUxEG7D10McaJHTL%2Fj7FmfI81s2uVXBitT%2BysBeHwCPcWMIU8tzkrqpDef0Y9Zdk1mZ1hqtm6NdLN2bpOyGcqWuSf2e24tHoFg3ogEag80W8QV82zBU2ly5Km; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a25683f8f46a51ff822c286a8ce860a1; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a25683f8f46a51ff822c286a8ce860a1; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a25683f8f46a51ff822c286a8ce860a1; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e0613f1c475ff3b04ef51fdcc9475a99; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - ZOLmoUN0EW7b9Et5CpH2 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - ZOLmoUN0EW7b9Et5CpH2 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:40:03 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:40:03 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=b512797bec7e57ac57411d3caba052b4; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=qvfdwqbLgBjknkQw7zkcSU%2BYaQ8NME7F3BmtrqJmG7jsOHKIenPdqOdXnr8UJoNY%2FLeBGHq1QOvunKLiiy%2FZ9r528P%2F3ZRqAyNwm4Li74xzPuBB8YzKjF5pjMM7PVUdt; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=b512797bec7e57ac57411d3caba052b4; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=b512797bec7e57ac57411d3caba052b4; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=b512797bec7e57ac57411d3caba052b4; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f6b364004d548dbeac45c47968705851; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - V1kafiU4WXE0ZSoVLkzl + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - V1kafiU4WXE0ZSoVLkzl + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:40:03 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_public.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_public.yml new file mode 100644 index 00000000000..851f81d431e --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/managed_folder_set_permissions_public.yml @@ -0,0 +1,1708 @@ +--- +http_interactions: +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:42:38 GMT + Oc-Fileid: + - '00000876ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d499387faab68e006dd8ff890885b6d4; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=GXD5Tv8wXbQ8giQuf%2BpDJrMoJuh2ZqBCoAFiwxm%2FGUprfFyEuTFiGYvCERZNMqH9O7y7MLsxGDSek0hkbElXUi%2F0w9HnD0muGazegsTZF0fFn1oR%2F5rj7G5Sq59aE4nt; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d499387faab68e006dd8ff890885b6d4; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d499387faab68e006dd8ff890885b6d4; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d499387faab68e006dd8ff890885b6d4; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=355e94e331c562888ca73f4e14b7bb29; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - 0ioHkdvXtrsbaPCtGsc4 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - 0ioHkdvXtrsbaPCtGsc4 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:42:38 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:38 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=e357e7f3d0aaac573298195de669d50f; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2F%2FFTyX534b5i2RgW46jZJMRkTkrvX%2BbnvBVj4ewsyDcHez%2BAqZtMOoY2hkGXhZWil3L5Mxu3uIbsv6UCZEe2xn6JEPqaULNsoakEWcPyLG%2B8yuyXM3wc376Gt14yjKQT; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e357e7f3d0aaac573298195de669d50f; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=e357e7f3d0aaac573298195de669d50f; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=e357e7f3d0aaac573298195de669d50f; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ea9176b26c8ba55a7fd220ce3f84297f; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - D1IeCYvrqS7qo8JKfW0c + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - D1IeCYvrqS7qo8JKfW0c + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '361' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/8760Fri, 02 May 2025 17:42:38 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:42:38 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:42:38 GMT + Oc-Fileid: + - '00000877ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=caefd2136456c4adca454cea46a4bb16; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=t%2B30ajw1XmjumF8fSPRiKo5U%2BaQvApwQ0C559dKwx%2F2tIw%2BunZyTf43dH6P0auQGvP4jbRS024waXH4oCIq2TfDOIPa4md8fVGl041MTSdJtupR3%2BVIQuIZa8Gxtdr1E; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=caefd2136456c4adca454cea46a4bb16; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=caefd2136456c4adca454cea46a4bb16; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=caefd2136456c4adca454cea46a4bb16; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=11c832fc3fc6dbf7980127c269c71548; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - iyaAcyWGJS9mc6cb6AWQ + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - iyaAcyWGJS9mc6cb6AWQ + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:42:38 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:38 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=c22b2225005e1b4df17b49f048534f3a; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=efViKBS%2F%2FWyEJvTQR9e5oM4e1RKhO0RKjLpud7IZRNuSAWJLcOgJp0opD0m5GHAd9A%2F4R2UgWyf8DPNB9Xg8JH%2B27ImWsEHWRBRZ9kaefuTrt8otkkpiHe9yNtXAUA39; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c22b2225005e1b4df17b49f048534f3a; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=c22b2225005e1b4df17b49f048534f3a; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=c22b2225005e1b4df17b49f048534f3a; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c95bfcfb61f0d1259b094968a3a402b6; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - XPrlaAbEK5399uWiGiGw + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - XPrlaAbEK5399uWiGiGw + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '367' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)/8770Fri, 02 May 2025 17:42:38 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:42:38 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Oc-Fileid: + - '00000878ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9356497c3c90e42a3742e4b9f54ce5b3; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=onsgkK%2BtTLjcgxTQJ9oa8zQD7xo8Jv6vhG5WzXVEtVsE8%2B0yfaHD9JXwNOEJRN8nFza5aW1726ACh0D%2BHJjNFELKRmlqXcj9EMy5XkmDd6DD8C7AcOfqaik1x%2Bk05Pgj; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9356497c3c90e42a3742e4b9f54ce5b3; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9356497c3c90e42a3742e4b9f54ce5b3; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=9356497c3c90e42a3742e4b9f54ce5b3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a6ef09f4d7027f4f8bdde3005f7a57d8; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - UBNdVbCuQ14ONGswqUvL + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - UBNdVbCuQ14ONGswqUvL + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=0e3f5bbc4ab5b9d5561ed57699762721; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=GNUAL65zg6zLBzD%2F3i%2FmAiHB3ErG7YJXWB6wtrCqyZajtZF9vqMubsuhlAu52se5VLo%2FgetvgirH0L5u72S0DpsJ57GUjondjA3oM7GUxc7IqVTXMsAQuEB66eqO8HZn; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=0e3f5bbc4ab5b9d5561ed57699762721; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=0e3f5bbc4ab5b9d5561ed57699762721; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=0e3f5bbc4ab5b9d5561ed57699762721; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=bef5adcd064e93f58ddf6959e2b6f756; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - pugdfkHqAW6icbCbjSEy + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - pugdfkHqAW6icbCbjSEy + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '362' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT%21%20f0r%20r34lz%20(-273)/8780Fri, 02 May 2025 17:42:39 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Oc-Fileid: + - '00000879ocxxq957y92g' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=40ad5ff6bc4c14a0a1d7f8699db8b044; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=x4u1NIo0%2FjYSAEW%2Fnz0Hbr%2FNpISXs5UKH%2FoSkXPtic%2F6AI0hc9sD8ZYGP%2FrOgRkftvvvv2B45hmUg4MlYgaZde3KRIo%2B3qHUCJayuyfhebq0oastr%2BTp20d0o%2Be725Df; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=40ad5ff6bc4c14a0a1d7f8699db8b044; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=40ad5ff6bc4c14a0a1d7f8699db8b044; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=40ad5ff6bc4c14a0a1d7f8699db8b044; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a5c6b765ba3a9cb068fb65167097572c; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - JEFaRqoyUZUB7bscVvN8 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - JEFaRqoyUZUB7bscVvN8 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9655b4d33dbd1c5c0bf7c5008e1e267d; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=nw6z8XHTp6B8zj6KfzMr5AGjVCDKY9ihGYMBpkvi04AxWVSLpHXkE6l1TuTYz6vbpaZ6APX1LB1xI%2F4EOKFEZTzvL4RwmdUYdSKwth3GnX%2BAvhGO798%2Fhzx8YwKn8Ufd; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9655b4d33dbd1c5c0bf7c5008e1e267d; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9655b4d33dbd1c5c0bf7c5008e1e267d; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=9655b4d33dbd1c5c0bf7c5008e1e267d; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8f8bfbe3daeba3c3ddf5fdd2c4dfa01e; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - PU4V2OOgdJHbHu8KL8FI + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - PU4V2OOgdJHbHu8KL8FI + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '346' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/8790Fri, 02 May 2025 17:42:39 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/876 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=b751bcc33632dab3da41c33474cebd44; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=lnbN%2FnTAH083SNN3VUfJSoY6B7JwBSEOXQwXnBTC49EuXypfZCt75oUA1v0eh3G9P9oQxkaSYooXxUJGxAV%2B0SObYYwEHkG91oBxRsqlFC7i6OPpVLfEKjiWgrw%2FQaWv; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=b751bcc33632dab3da41c33474cebd44; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=b751bcc33632dab3da41c33474cebd44; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=b751bcc33632dab3da41c33474cebd44; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=46697da7a0422fa42b9a25296b046025; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - QbpWZKOOtjJF60jxnhoO + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '282' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":876,"name":"[Sample] + Project Name | Ehuu (-273)","mtime":1746207758,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/[Sample] + Project Name | Ehuu (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + leia + 31 + 3 + + + user + luke + 31 + 3 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1419' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=965db6cfc1ce1352e5355278e8164773; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=n%2FpVzjW%2BmT%2BYCI9IU9L3VbIo7iHYRXwzdtNutWgqcIVHgb02obDHbmDe3meIEAqrDjpd4irrK%2B3Lmjux9KPcTtm8MfSe0pepGbuag1HOdRiySmYsFrgU8W%2FnlIk7ZbZW; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=965db6cfc1ce1352e5355278e8164773; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=965db6cfc1ce1352e5355278e8164773; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=965db6cfc1ce1352e5355278e8164773; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=7f4be1c673f32155a00ff08b0880ad47; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - UYa0eGyLuzKz6CsNtZOz + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - UYa0eGyLuzKz6CsNtZOz + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '402' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/877 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=80955eb782c2f277642c098356ee71b3; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=k15kb7fottkcJorDd3LYyJii7pVJYEXhydSH2J43SRsEwahOpxwvSLOi8P6ZA%2FO6TPboTkpr7Ip2GFf7WZIQb2uOtITw4v9jIJgPnKl%2Bata7MFoHF%2B6iqapjPM0aeJir; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=80955eb782c2f277642c098356ee71b3; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=80955eb782c2f277642c098356ee71b3; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=80955eb782c2f277642c098356ee71b3; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f6e8d8b0e6b6e494363084e80a6b8775; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - zadxDHADjiZNl7W23Slr + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '294' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":877,"name":"\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)","mtime":1746207758,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/\u003C=o=\u003E + | \"Jedi\" Project Folder ||| (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + leia + 31 + 3 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1179' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a1128ed8ad22a5a7f7390b4b58f391ca; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=ERbVWu2GFnH%2F0Hf9%2F2%2BwYZQ%2BilMlhu4Az8UJ2TX2NESnqt8G4gPCt11XZpQhyeUxxZt%2F3a%2FayGBVvjKzZXFrPXeIMV5PmX%2Fr2cx0uSscePGAgT%2F6RlRA14EkKOHQ0mjS; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a1128ed8ad22a5a7f7390b4b58f391ca; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a1128ed8ad22a5a7f7390b4b58f391ca; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=a1128ed8ad22a5a7f7390b4b58f391ca; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=20b622508dfd833c4a038be64969d14a; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - fztL3jwlZO2zWTr7Lex1 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - fztL3jwlZO2zWTr7Lex1 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '423' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%3c%3do%3d%3e%20%7c%20%22Jedi%22%20Project%20Folder%20%7c%7c%7c%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/879 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=e2ebacdf1bdf6d7c11e4b1027edc6e82; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=dDyh8imx4JuoOLyJ84nAsueFF6fUfqRdnSu9dvbHTBySFxDiLssVfu02E19h%2Bd7ouinOJPm%2FNHeqDE%2F0XyNdoXDO1wAw7bHKnnVvK%2BuZs32D3PaMP9Av6vwqH8HUBapC; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e2ebacdf1bdf6d7c11e4b1027edc6e82; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=e2ebacdf1bdf6d7c11e4b1027edc6e82; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=e2ebacdf1bdf6d7c11e4b1027edc6e82; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=23f103040c1ab6d81c2c32844e63fe30; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - GPWWpAiFTnnh0dlyYxOn + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '276' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":879,"name":"PUBLIC + PROJECT (-273)","mtime":1746207759,"ctime":0,"mimetype":"application\/x-op-directory","size":0,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/PUBLIC + PROJECT (-273)\/"}}}' + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 0 + + + user + OpenProject + 31 + 31 + + + user + anakin + 31 + 31 + + + user + admin + 31 + 1 + + + user + leia + 31 + 1 + + + user + luke + 31 + 1 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '1660' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=ea4d37f27d27f2a810ccac3924d7ccee; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2FwkMCHH6hSW2jaabRQLxkrerSmx3rjfD6xXIJazGTbSo9trnP3YQeQL5BbpmlAudRnlf6%2BtHXfpvpSkK7bpW4EvA6Iqo8uKnAIZjq3A97b3YtA%2B%2BrgokjAv8M8pNoYlx; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=ea4d37f27d27f2a810ccac3924d7ccee; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=ea4d37f27d27f2a810ccac3924d7ccee; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=ea4d37f27d27f2a810ccac3924d7ccee; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=00e7da43c74c519915d0a7fed1adf5b2; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - HynNmFTNJA5meH9M71QA + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - HynNmFTNJA5meH9M71QA + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '376' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:42:39 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/cloud/groups/OpenProject + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:39 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=3609652bdddea802f735d1d99b1ee5c4; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=EXmLsrDu%2Fi%2Btqfxs%2F%2BUdtP%2F20AssBlm%2BwlU%2F9IkE6r0k1Gu%2BO7PEuqJ2UIUXmdpJgZ5npak3IXX86XPYS5GFgQWaHfKkHzxEdXy%2FLaCD6BQlFdFbpKESC8GGr4%2BW8ycM; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3609652bdddea802f735d1d99b1ee5c4; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=3609652bdddea802f735d1d99b1ee5c4; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=3609652bdddea802f735d1d99b1ee5c4; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8803fbd4fbbb7a738fd938d66f8a11a9; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - RnXjm5e4zCfuJimY2ZNT + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '186' + body: + encoding: UTF-8 + string: | + + + + ok + 100 + OK + + + + + + admin + anakin + leia + luke + OpenProject + + + + recorded_at: Fri, 02 May 2025 17:42:40 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '141' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Fri, 02 May 2025 17:42:40 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=22370be7611d423c6e8831a0902d8612; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=pDKh%2BjHHH3Na%2FeiT2%2B%2BCueb4VbmWWL%2B61Y31GilHnpXZBwT7R5FCORdI%2F%2FRfTlO0YlBmQLX3lC86VngifiG3%2BYTye%2F2k1NxEYrgzWWAaWHkufBDgSW1bLTc1aw3e2Pe6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=22370be7611d423c6e8831a0902d8612; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=22370be7611d423c6e8831a0902d8612; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=22370be7611d423c6e8831a0902d8612; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e30b7355617048e137570c7eadb0cbd1; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - rY9Jg2IS1BKm2LaIjKZp + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - rY9Jg2IS1BKm2LaIjKZp + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '383' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273)/groupOpenProjectOpenProject310userOpenProjectOpenProject3131useranakinanakin3131useradminadmin311userleialeia311userlukeluke311HTTP/1.1 200 OK + recorded_at: Fri, 02 May 2025 17:42:40 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:42:40 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=8ff17957ef3e5924bcf989152b869ad2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=TpGtpHBWb6EZ95S0sBMZaSn98YHoPFPBgi1M5tyNmLGRhnwvvAQ6gSqIuuddxgvcbCs6IgomS62SKTUTjEHSNQfWXpsYH8gipoQtPHUCinofhgCrb4pNLgNdJsCHdvlE; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=8ff17957ef3e5924bcf989152b869ad2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=8ff17957ef3e5924bcf989152b869ad2; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=8ff17957ef3e5924bcf989152b869ad2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=e2bbf8d3abbcf184d224e224d5c0b3c0; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - tclosJru9q9JRj6ujIZF + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - tclosJru9q9JRj6ujIZF + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:42:40 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%3C=o=%3E%20%7C%20%22Jedi%22%20Project%20Folder%20%7C%7C%7C%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:42:40 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=302824c8b40ce8235a04193d47cad2aa; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=lXGX5lNthCMfreTIwEhCXxnk%2F6HzOxBgWKR1rpsZErcIEKnIliUr9RlZYv4QQgGSp6TBM0VdeOF9dTuOL0k7bReDgM7EzJiaQsq6ZOmgfuho7NnZZ1PQZmKtvjZ%2FSuRc; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=302824c8b40ce8235a04193d47cad2aa; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=302824c8b40ce8235a04193d47cad2aa; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=302824c8b40ce8235a04193d47cad2aa; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=62cd236f7a19ff8cac5e2230176b2b21; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - lh0tkT14VUkSBtkHooGp + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - lh0tkT14VUkSBtkHooGp + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:42:40 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/INACTIVE%20PROJECT!%20f0r%20r34lz%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:42:40 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=d866ee894317616937773b6aabc66f80; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=qUYCSyJyeWxFk3nvdNzQ%2BaUvU5Kv9xYNWvwCy6VSsfpWvSQjTcH%2F6CXzmu3xFijI2ayQ2aD8QwAfJTxMYbPOUoJud%2B60Xhsq%2F4RNP7c4BwlCrGSm987bvRswln6y571h; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d866ee894317616937773b6aabc66f80; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=d866ee894317616937773b6aabc66f80; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=d866ee894317616937773b6aabc66f80; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=314c4fef44e6364fb29aae932017478e; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - ZfWZZiL84NhxyKDEPnB0 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - ZfWZZiL84NhxyKDEPnB0 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:42:40 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/PUBLIC%20PROJECT%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Content-Security-Policy: + - default-src 'none'; + Date: + - Fri, 02 May 2025 17:42:40 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=cd3284ab7ceacd3c5b4001b0ec7a3864; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=PPDMGyveoqji42hoizr%2FoFuItGU%2F9NI2Qm9Bo6UclHum%2BTtH%2BvMZ519hYcv5F2nSEYJB6%2FOfN7vOzRa40kyfrVGckgOkG70RA4xznJDXIdtNu%2BdhFHJyaUgodth73HiB; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=cd3284ab7ceacd3c5b4001b0ec7a3864; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=cd3284ab7ceacd3c5b4001b0ec7a3864; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocxxq957y92g=cd3284ab7ceacd3c5b4001b0ec7a3864; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=d833106ef85f51ec8b9fbab9d688d635; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - MpQm7iMy4M4B1wsfPlzu + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - MpQm7iMy4M4B1wsfPlzu + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '' + recorded_at: Fri, 02 May 2025 17:42:40 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/sync_service_creation_fail.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/sync_service_creation_fail.yml new file mode 100644 index 00000000000..4bd712809a2 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/sync_service_creation_fail.yml @@ -0,0 +1,537 @@ +--- +http_interactions: +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.4.0 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 201 + message: Created + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - text/html; charset=UTF-8 + Date: + - Tue, 21 Jan 2025 17:09:16 GMT + Oc-Fileid: + - 00000662ocxxq957y92g + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=6a4dc7084fa34a1711338f7f745464b6; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=K37QVOPW03YQiLauKSCZ5bzqy2sAwGsWIjAladNMAPdbEUycG2mYyY48mNviGLBO4X%2FKYR9XS8soFfzG1hKY3iKgFLBPgf8ixPiF%2FwOWDIWessD0Dc0fcuR7jhG8XLl4; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6a4dc7084fa34a1711338f7f745464b6; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=6a4dc7084fa34a1711338f7f745464b6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=6a4dc7084fa34a1711338f7f745464b6; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=3b15dffb9b1fc55521d02f47ab6a7911; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - TKED0ckv35pd0eMqME69 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - TKED0ckv35pd0eMqME69 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '0' + body: + encoding: UTF-8 + string: '' + recorded_at: Tue, 21 Jan 2025 17:09:16 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: UTF-8 + string: | + + + + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.4.0 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '229' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Tue, 21 Jan 2025 17:09:16 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=1efaad59ce5f6f27b82899def8495a31; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=0cI%2FAzxfTeFPoI7SkSsfloFTyFgmEEqDzBHWdBwHqsAnbPYJ8npvS6PwARoA0Wt8NoLocl6vTNTPbkNL5fWKAgX10MKdf32S3GB5%2BqDZHdYeAzJtywr5i6n7p6DZFZdQ; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1efaad59ce5f6f27b82899def8495a31; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=1efaad59ce5f6f27b82899def8495a31; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1efaad59ce5f6f27b82899def8495a31; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=c20f161bb9618d09e940914a6f72b9c1; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - mRZbsL7FCXqJOt8WKx71 + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - mRZbsL7FCXqJOt8WKx71 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '360' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/6620Tue, 21 Jan 2025 17:09:16 GMTRMGDNVCKOpenProjectHTTP/1.1 200 OK + recorded_at: Tue, 21 Jan 2025 17:09:16 GMT +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.4.0 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '192' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Tue, 21 Jan 2025 17:09:16 GMT + Dav: + - 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, + nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=08e94660fca905807cab1390b0a1c934; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=fUkxnNLKCGS9E8%2Fc%2BnBM%2F1hcqdng1h304r%2FNDeKHynYjMAvDonyVO6H%2FoYFUxr2WoBJViv6W%2Bfa2TnRUp019FAKNHGEXK03tSFoabUUjvapeX%2BykK7D28etDGyyq12UV; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=08e94660fca905807cab1390b0a1c934; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=08e94660fca905807cab1390b0a1c934; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=08e94660fca905807cab1390b0a1c934; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=f60c91114e2d165346c8c9b70a133816; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - yBFwfleC9ZbljVcYBw7g + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - yBFwfleC9ZbljVcYBw7g + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '466' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProject/103groupOpenProjectOpenProject311userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/Demo%20project%20(1)/180groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/%5bdev%5d%20Empty%20(3)/181groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/Projectify%20(8)/204groupOpenProjectOpenProject310userOpenProjectOpenProject3131HTTP/1.1 200 OK/remote.php/dav/files/OpenProject/OpenProject/%5bSample%5d%20Project%20Name%20%7c%20Ehuu%20(-273)/662HTTP/1.1 200 OKHTTP/1.1 404 Not Found + recorded_at: Tue, 21 Jan 2025 17:09:16 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/apps/integration_openproject/fileinfo/103 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + Ocs-Apirequest: + - 'true' + Accept: + - application/json + User-Agent: + - httpx.rb/1.4.0 + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 21 Jan 2025 17:09:16 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=11debc91718deb3262ad1be585e1c9bc; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=J4kItkpIeE83mfet%2FwRBlIRv3eviW27uajC%2B0KeL%2B5u6lvcJbq1kTPybpnJnldDDAufgdKB%2BR8wIlhY7BfnDXQMsk9x7NjQ76Sgd6e0XXzt%2FcTow5xLMLQ83iBj9KhOg; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=11debc91718deb3262ad1be585e1c9bc; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=11debc91718deb3262ad1be585e1c9bc; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=11debc91718deb3262ad1be585e1c9bc; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=1a7d4c8f29f0c3f4e90abcac3fc820f0; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - BivQOQFKIC2m8IfSs2c8 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '251' + body: + encoding: UTF-8 + string: '{"ocs":{"meta":{"status":"ok","statuscode":100,"message":"OK","totalitems":"","itemsperpage":""},"data":{"status":"OK","statuscode":200,"id":103,"name":"OpenProject","mtime":1737479356,"ctime":0,"mimetype":"application\/x-op-directory","size":90335,"owner_name":"OpenProject","owner_id":"OpenProject","modifier_name":null,"modifier_id":null,"dav_permissions":"RMGDNVCK","path":"files\/OpenProject\/"}}}' + recorded_at: Tue, 21 Jan 2025 17:09:16 GMT +- request: + method: proppatch + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + group + OpenProject + 31 + 1 + + + user + OpenProject + 31 + 31 + + + + + + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.4.0 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '696' + response: + status: + code: 207 + message: Multi-Status + headers: + Content-Security-Policy: + - default-src 'none'; + Content-Type: + - application/xml; charset=utf-8 + Date: + - Tue, 21 Jan 2025 17:09:16 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=97e948ccc8c4f318b9f4c52631d31ba7; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=XDlYwKiczit1OtQACBXsc3bnFR6nky%2F7mNTzVL4q204aRsDDGlIeNguLKacGYB71CjkaCf%2F9jzpmJ2AeJkQITPlt6mpFVp7Kw3nLRvjKvqA555q7ZLXVbmdE64nEllzt; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=97e948ccc8c4f318b9f4c52631d31ba7; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=97e948ccc8c4f318b9f4c52631d31ba7; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=97e948ccc8c4f318b9f4c52631d31ba7; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9a2083955ef2dcfc1ee7eb7a33c89ba9; + path=/; secure; HttpOnly; SameSite=Lax + Vary: + - Brief,Prefer + X-Content-Type-Options: + - nosniff + X-Debug-Token: + - yn94gj7cWhPnFyK6VCFB + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Request-Id: + - yn94gj7cWhPnFyK6VCFB + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '350' + body: + encoding: UTF-8 + string: | + + /remote.php/dav/files/OpenProject/OpenProjectHTTP/1.1 200 OK + recorded_at: Tue, 21 Jan 2025 17:09:16 GMT +- request: + method: mkcol + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject/%5BSample%5D%20Project%20Name%20%7C%20Ehuu%20(-273) + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.4.0 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 405 + message: Method Not Allowed + headers: + Allow: + - OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT + Content-Security-Policy: + - 'default-src ''none'';base-uri ''none'';manifest-src ''self'';script-src ''self'';style-src + ''self'' ''unsafe-inline'';img-src ''self'' data: blob:;font-src ''self'' + data:;connect-src ''self'';media-src ''self'';frame-ancestors ''self'';form-action + ''self''' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Tue, 21 Jan 2025 17:09:16 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=a393e3b443d6cc63f8470e99442c1533; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=YuPjF7DG5KsyccDA%2BYh7VvFnJL0kAD69KIyGhoCpHFEJlKY8TAupPbXzmPfUR%2BSSmULVPB2fic4crIatoAQy9yo4xV6PQvwjRcLOApcRgkky6N9ciqMtQWo9Y4B%2BS0ac; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a393e3b443d6cc63f8470e99442c1533; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=a393e3b443d6cc63f8470e99442c1533; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=a393e3b443d6cc63f8470e99442c1533; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=cae1c899dcfb6f0f525fab54a4d3f2a5; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '526' + body: + encoding: UTF-8 + string: "\n\n\tInternal Server Error\n\t\n\t\tThe + server was unable to complete your request.\t\tIf this happens again, please + send the technical details below to the server administrator.\t\tMore details + can be found in the server log.\t\t\t\n\n\t\n\t\t127.0.0.1\n\t\tZnsznBmdibTtOyF59aPG\n\n\t\t\n\n" + recorded_at: Tue, 21 Jan 2025 17:09:16 GMT +- request: + method: delete + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/662 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Basic + User-Agent: + - httpx.rb/1.4.0 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 404 + message: Not Found + headers: + Content-Security-Policy: + - 'default-src ''none'';base-uri ''none'';manifest-src ''self'';script-src ''self'';style-src + ''self'' ''unsafe-inline'';img-src ''self'' data: blob:;font-src ''self'' + data:;connect-src ''self'';media-src ''self'';frame-ancestors ''self'';form-action + ''self''' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Tue, 21 Jan 2025 17:09:17 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=9f6873750380bffdd52522d28334d1ad; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=gMfF9P1bCfLoHSNzOftrl8SJ2rWt9YqlLSbiTw5otbCWalTFbeqvTeGShLgbQun8GAq9VdQKUA2KDUAZBptUA8AMAAA%2F91p%2BRySXp%2B7CV%2FJGfX0s4AMNyOSDBS1yGYdn; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9f6873750380bffdd52522d28334d1ad; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=9f6873750380bffdd52522d28334d1ad; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=9f6873750380bffdd52522d28334d1ad; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=676848016351f98c4973b04bde9fd2bb; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '526' + body: + encoding: UTF-8 + string: "\n\n\tInternal Server Error\n\t\n\t\tThe + server was unable to complete your request.\t\tIf this happens again, please + send the technical details below to the server administrator.\t\tMore details + can be found in the server log.\t\t\t\n\n\t\n\t\t127.0.0.1\n\t\tBt1BeMzgW1PoRryqdlUO\n\n\t\t\n\n" + recorded_at: Tue, 21 Jan 2025 17:09:17 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/sync_service_root_read_failure.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/sync_service_root_read_failure.yml new file mode 100644 index 00000000000..9e401096e55 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/nextcloud/sync_service_root_read_failure.yml @@ -0,0 +1,84 @@ +--- +http_interactions: +- request: + method: propfind + uri: https://nextcloud.local/remote.php/dav/files/OpenProject/OpenProject + body: + encoding: UTF-8 + string: | + + + + + + + + headers: + Authorization: + - Basic + Depth: + - '1' + User-Agent: + - httpx.rb/1.4.0 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/xml; charset=utf-8 + Content-Length: + - '192' + response: + status: + code: 401 + message: Unauthorized + headers: + Content-Security-Policy: + - 'default-src ''none'';base-uri ''none'';manifest-src ''self'';script-src ''self'';style-src + ''self'' ''unsafe-inline'';img-src ''self'' data: blob:;font-src ''self'' + data:;connect-src ''self'';media-src ''self'';frame-ancestors ''self'';form-action + ''self''' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Tue, 21 Jan 2025 17:09:15 GMT + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.62 (Debian) + Set-Cookie: + - ocxxq957y92g=af21f0cd81b24375a97bcbe0a3c2acc2; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=%2BDszHPEo1sKpOgyzAJLyCFHCloRBZpf2SVswQECeuTbUsRoAOT6sAFC%2BAZ4SAS14MAylH4%2BMuOD4ZmDnbBw7mRRXySzh8F3Sn5tQN1KGp0Sa4Q44K9K92Uyd8Tm%2B8zes; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=af21f0cd81b24375a97bcbe0a3c2acc2; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocxxq957y92g=af21f0cd81b24375a97bcbe0a3c2acc2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=af21f0cd81b24375a97bcbe0a3c2acc2; + path=/; secure; HttpOnly; SameSite=Lax, ocxxq957y92g=46a5bfff52644a7f3df4f8b309a63696; + path=/; secure; HttpOnly; SameSite=Lax + Www-Authenticate: + - Basic realm="Nextcloud", charset="UTF-8" + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.2.27 + X-Robots-Tag: + - noindex, nofollow + X-Xss-Protection: + - 1; mode=block + Content-Length: + - '526' + body: + encoding: UTF-8 + string: "\n\n\tInternal Server Error\n\t\n\t\tThe + server was unable to complete your request.\t\tIf this happens again, please + send the technical details below to the server administrator.\t\tMore details + can be found in the server log.\t\t\t\n\n\t\n\t\t127.0.0.1\n\t\tL0ofwugXtxKf7tEqEnOJ\n\n\t\t\n\n" + recorded_at: Tue, 21 Jan 2025 17:09:16 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/payloads/empty_folder.json b/modules/storages/spec/support/payloads/empty_folder.json deleted file mode 100644 index 15b961159cc..00000000000 --- a/modules/storages/spec/support/payloads/empty_folder.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives('b%21-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd')/root/children", - "value": [] -} diff --git a/modules/storages/spec/support/payloads/file_drive_item_1.json b/modules/storages/spec/support/payloads/file_drive_item_1.json deleted file mode 100644 index 62d1d72aa9c..00000000000 --- a/modules/storages/spec/support/payloads/file_drive_item_1.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('48d31887-5fad-4d73-a9f5-3c356e68a038')/drive/items/$entity", - "@microsoft.graph.downloadUrl": "https://m365x214355-my.sharepoint.com/personal/meganb_m365x214355_onmicrosoft_com/_layouts/15/download.aspx?UniqueId=b2cad8f3-d3cb-4f14-a801-f6bafa078d64&Translate=false&tempauth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAvbTM2NXgyMTQzNTUtbXkuc2hhcmVwb2ludC5jb21AZGNkMjE5ZGQtYmM2OC00YjliLWJmMGItNGEzM2E3OTZiZTM1IiwiaXNzIjoiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwIiwibmJmIjoiMTY5NTIwNzI4MSIsImV4cCI6IjE2OTUyMTA4ODEiLCJlbmRwb2ludHVybCI6Ii85bTNxeGlHT1ZBQWxldjhkMmttNG96aktvck55VGlrdTYrZzRQNTFJUnM9IiwiZW5kcG9pbnR1cmxMZW5ndGgiOiIxNjkiLCJpc2xvb3BiYWNrIjoiVHJ1ZSIsImNpZCI6IkttRk1WN3JTVzBDS1RJRzNFRVArY1E9PSIsInZlciI6Imhhc2hlZHByb29mdG9rZW4iLCJzaXRlaWQiOiJaRGd5TXpFeVpqa3RZakl6WWkwMFkySmpMVGsxWkRVdE0yVXdaRGswWlRZNFl6RmwiLCJhcHBfZGlzcGxheW5hbWUiOiJhcGlzYW5kYm94cHJveHkiLCJnaXZlbl9uYW1lIjoiTWVnYW4iLCJmYW1pbHlfbmFtZSI6IkJvd2VuIiwiYXBwaWQiOiIwNWIxMGEyZC02MmRiLTQyMGMtODYyNi01NWYzYTVlNzg2NWIiLCJ0aWQiOiJkY2QyMTlkZC1iYzY4LTRiOWItYmYwYi00YTMzYTc5NmJlMzUiLCJ1cG4iOiJtZWdhbmJAbTM2NXgyMTQzNTUub25taWNyb3NvZnQuY29tIiwicHVpZCI6IjEwMDNCRkZEQTM4MTMxQUYiLCJjYWNoZWtleSI6IjBoLmZ8bWVtYmVyc2hpcHwxMDAzYmZmZGEzODEzMWFmQGxpdmUuY29tIiwic2NwIjoibXlmaWxlcy5yZWFkIGdyb3VwLnJlYWQgYWxsc2l0ZXMucmVhZCBhbGxwcm9maWxlcy5yZWFkIGFsbHByb2ZpbGVzLnJlYWQgdGVybXN0b3JlLnJlYWQiLCJ0dCI6IjIiLCJpcGFkZHIiOiI0MC4xMjYuNDEuOTYifQ._a-C996692KBUCD7GF5ZQtwoemiL_lw4qVMt0E7Jhzc&ApiVersion=2.0", - "createdDateTime": "2017-08-07T16:16:53Z", - "eTag": "\"{B2CAD8F3-D3CB-4F14-A801-F6BAFA078D64},2\"", - "id": "01BYE5RZ7T3DFLFS6TCRH2QAPWXL5APDLE", - "lastModifiedDateTime": "2017-08-07T16:16:53Z", - "name": "Popular Mixed Drinks.xlsx", - "webUrl": "https://m365x214355-my.sharepoint.com/personal/meganb_m365x214355_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BB2CAD8F3-D3CB-4F14-A801-F6BAFA078D64%7D&file=Popular%20Mixed%20Drinks.xlsx&action=default&mobileredirect=true", - "cTag": "\"c:{B2CAD8F3-D3CB-4F14-A801-F6BAFA078D64},1\"", - "size": 7064929, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ5MYLM2SMX75ZBIPQZIHT6OAYPB", - "path": "/drive/root:/Business Data", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "IQcTRezR55cAlOtZGiOF33CuqMU=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:16:53Z", - "lastModifiedDateTime": "2017-08-07T16:16:53Z" - } -} diff --git a/modules/storages/spec/support/payloads/file_drive_item_2.json b/modules/storages/spec/support/payloads/file_drive_item_2.json deleted file mode 100644 index 7aead5591ec..00000000000 --- a/modules/storages/spec/support/payloads/file_drive_item_2.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('48d31887-5fad-4d73-a9f5-3c356e68a038')/drive/items/$entity", - "@microsoft.graph.downloadUrl": "https://m365x214355-my.sharepoint.com/personal/meganb_m365x214355_onmicrosoft_com/_layouts/15/download.aspx?UniqueId=166a0295-565a-4143-a860-a56e7465eddd&Translate=false&tempauth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAvbTM2NXgyMTQzNTUtbXkuc2hhcmVwb2ludC5jb21AZGNkMjE5ZGQtYmM2OC00YjliLWJmMGItNGEzM2E3OTZiZTM1IiwiaXNzIjoiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwIiwibmJmIjoiMTY5NTIwNzMyMyIsImV4cCI6IjE2OTUyMTA5MjMiLCJlbmRwb2ludHVybCI6ImVJMm5GNENSQ05jKytya05oeGMrY0NLbXVIalJPZDBybVFLNTJCb1g3Mms9IiwiZW5kcG9pbnR1cmxMZW5ndGgiOiIxNjkiLCJpc2xvb3BiYWNrIjoiVHJ1ZSIsImNpZCI6Im1RSDA0RjdZaUVLUExOMlJYckMrL2c9PSIsInZlciI6Imhhc2hlZHByb29mdG9rZW4iLCJzaXRlaWQiOiJaRGd5TXpFeVpqa3RZakl6WWkwMFkySmpMVGsxWkRVdE0yVXdaRGswWlRZNFl6RmwiLCJhcHBfZGlzcGxheW5hbWUiOiJhcGlzYW5kYm94cHJveHkiLCJnaXZlbl9uYW1lIjoiTWVnYW4iLCJmYW1pbHlfbmFtZSI6IkJvd2VuIiwiYXBwaWQiOiIwNWIxMGEyZC02MmRiLTQyMGMtODYyNi01NWYzYTVlNzg2NWIiLCJ0aWQiOiJkY2QyMTlkZC1iYzY4LTRiOWItYmYwYi00YTMzYTc5NmJlMzUiLCJ1cG4iOiJtZWdhbmJAbTM2NXgyMTQzNTUub25taWNyb3NvZnQuY29tIiwicHVpZCI6IjEwMDNCRkZEQTM4MTMxQUYiLCJjYWNoZWtleSI6IjBoLmZ8bWVtYmVyc2hpcHwxMDAzYmZmZGEzODEzMWFmQGxpdmUuY29tIiwic2NwIjoibXlmaWxlcy5yZWFkIGdyb3VwLnJlYWQgYWxsc2l0ZXMucmVhZCBhbGxwcm9maWxlcy5yZWFkIGFsbHByb2ZpbGVzLnJlYWQgdGVybXN0b3JlLnJlYWQiLCJ0dCI6IjIiLCJpcGFkZHIiOiI0MC4xMjYuNDEuOTYifQ.mszZL3eBLbQwsCXjEhEbXONqlxLcjYs3Gw6YGO4s02Y&ApiVersion=2.0", - "createdDateTime": "2017-08-07T16:17:09Z", - "eTag": "\"{166A0295-565A-4143-A860-A56E7465EDDD},2\"", - "id": "01BYE5RZ4VAJVBMWSWINA2QYFFNZ2GL3O5", - "lastModifiedDateTime": "2017-08-07T16:17:09Z", - "name": "Retail Store Analysis.xlsx", - "webUrl": "https://m365x214355-my.sharepoint.com/personal/meganb_m365x214355_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7B166A0295-565A-4143-A860-A56E7465EDDD%7D&file=Retail%20Store%20Analysis.xlsx&action=default&mobileredirect=true", - "cTag": "\"c:{166A0295-565A-4143-A860-A56E7465EDDD},1\"", - "size": 11282101, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ5MYLM2SMX75ZBIPQZIHT6OAYPB", - "path": "/drive/root:/Business Data", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "r3UJbCEdsOVmLXQJQtBpJdUlXhA=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:17:09Z", - "lastModifiedDateTime": "2017-08-07T16:17:09Z" - } -} diff --git a/modules/storages/spec/support/payloads/folder_drive_item.json b/modules/storages/spec/support/payloads/folder_drive_item.json deleted file mode 100644 index b81c0b5012e..00000000000 --- a/modules/storages/spec/support/payloads/folder_drive_item.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('48d31887-5fad-4d73-a9f5-3c356e68a038')/drives('b%21-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd')/items/$entity", - "createdDateTime": "2017-08-07T16:16:30Z", - "eTag": "\"{A9D9C2AC-FF32-42EE-87C3-283CFCE061E1},1\"", - "id": "01BYE5RZ5MYLM2SMX75ZBIPQZIHT6OAYPB", - "lastModifiedDateTime": "2017-08-07T16:16:30Z", - "name": "Business Data", - "webUrl": "https://m365x214355-my.sharepoint.com/personal/meganb_m365x214355_onmicrosoft_com/Documents/Business%20Data", - "cTag": "\"c:{A9D9C2AC-FF32-42EE-87C3-283CFCE061E1},0\"", - "size": 39566226, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:16:30Z", - "lastModifiedDateTime": "2017-08-07T16:16:30Z" - }, - "folder": { - "childCount": 6 - } -} diff --git a/modules/storages/spec/support/payloads/root_drive.json b/modules/storages/spec/support/payloads/root_drive.json deleted file mode 100644 index 54e1ca0b727..00000000000 --- a/modules/storages/spec/support/payloads/root_drive.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('48d31887-5fad-4d73-a9f5-3c356e68a038')/drive/root/$entity", - "createdDateTime": "2017-07-27T02:41:36Z", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "lastModifiedDateTime": "2023-09-15T04:55:34Z", - "name": "root", - "webUrl": "https://m365x214355-my.sharepoint.com/personal/meganb_m365x214355_onmicrosoft_com/Documents", - "size": 106329756, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd" - }, - "fileSystemInfo": { - "createdDateTime": "2017-07-27T02:41:36Z", - "lastModifiedDateTime": "2023-09-15T04:55:34Z" - }, - "folder": { - "childCount": 38 - }, - "root": {} -} diff --git a/modules/storages/spec/support/payloads/root_drive_children.json b/modules/storages/spec/support/payloads/root_drive_children.json deleted file mode 100644 index ea02a4f4e51..00000000000 --- a/modules/storages/spec/support/payloads/root_drive_children.json +++ /dev/null @@ -1,1395 +0,0 @@ -{ - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('48d31887-5fad-4d73-a9f5-3c356e68a038')/drive/root/children(id,name,size,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference)", - "value": [ - { - "@odata.etag": "\"{60F36ED0-85CE-4771-B349-E671C691EFCA},1\"", - "id": "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36K", - "name": "Attachments", - "size": 0, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-07-31T18:56:29Z", - "lastModifiedDateTime": "2017-07-31T18:56:29Z" - }, - "folder": { - "childCount": 0 - } - }, - { - "@odata.etag": "\"{A9D9C2AC-FF32-42EE-87C3-283CFCE061E1},1\"", - "id": "01BYE5RZ5MYLM2SMX75ZBIPQZIHT6OAYPB", - "name": "Business Data", - "size": 39566226, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:16:30Z", - "lastModifiedDateTime": "2017-08-07T16:16:30Z" - }, - "folder": { - "childCount": 6 - } - }, - { - "@odata.etag": "\"{73E9E609-FA05-42D8-82BD-1239D821CDD8},1\"", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "name": "Class Documents", - "size": 16651792, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:10:22Z", - "lastModifiedDateTime": "2017-08-07T16:10:22Z" - }, - "folder": { - "childCount": 22 - } - }, - { - "@odata.etag": "\"{70BB7882-79BA-4711-A14F-F2C95A811302},1\"", - "id": "01BYE5RZ4CPC5XBOTZCFD2CT7SZFNICEYC", - "name": "Contoso Clothing", - "size": 5912877, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:18:24Z", - "lastModifiedDateTime": "2017-08-07T16:18:24Z" - }, - "folder": { - "childCount": 14 - } - }, - { - "@odata.etag": "\"{D43D7B05-A00E-4889-B2AD-B3BC79950023},1\"", - "id": "01BYE5RZYFPM65IDVARFELFLNTXR4ZKABD", - "name": "Contoso Electronics", - "size": 18128460, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:05:29Z", - "lastModifiedDateTime": "2017-08-07T16:05:29Z" - }, - "folder": { - "childCount": 6 - } - }, - { - "@odata.etag": "\"{704F02D3-CC74-43B6-A38D-63FC9A4B94B4},1\"", - "id": "01BYE5RZ6TAJHXA5GMWZB2HDLD7SNEXFFU", - "name": "CR-227 Project", - "size": 6934759, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:17:40Z", - "lastModifiedDateTime": "2017-08-07T16:17:40Z" - }, - "folder": { - "childCount": 5 - } - }, - { - "@odata.etag": "\"{2BB874B8-B62C-4074-9E53-DB9081B4CFAF},1\"", - "id": "01BYE5RZ5YOS4CWLFWORAJ4U63SCA3JT5P", - "name": "Notebooks", - "size": 22842, - "createdBy": { - "application": { - "id": "2d4d3d8e-2be3-4bef-9f87-7875a61c29de", - "displayName": "OneNote" - }, - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "application": { - "id": "2d4d3d8e-2be3-4bef-9f87-7875a61c29de", - "displayName": "OneNote" - }, - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-09-15T01:15:43Z", - "lastModifiedDateTime": "2017-09-15T01:15:43Z" - }, - "folder": { - "childCount": 2 - } - }, - { - "@odata.etag": "\"{748556DB-5188-404B-BFCF-F5151ED9EB24},1\"", - "id": "01BYE5RZ63K2CXJCCRJNAL7T7VCUPNT2ZE", - "name": "Presentation Documents", - "size": 11905701, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:13:50Z", - "lastModifiedDateTime": "2017-08-07T16:13:50Z" - }, - "folder": { - "childCount": 20 - } - }, - { - "@odata.etag": "\"{D7A8A9D0-3D78-4693-ABBD-5322CA2D9903},1\"", - "id": "01BYE5RZ6QVGUNO6B5SNDKXPKTELFC3GID", - "name": "Private Info", - "size": 20674, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:06:56Z", - "lastModifiedDateTime": "2017-08-07T16:06:56Z" - }, - "folder": { - "childCount": 2 - } - }, - { - "@odata.etag": "\"{C5B6EF53-3783-453F-9ACA-6CE8BE81A474},1\"", - "id": "01BYE5RZ2T563MLAZXH5CZVSTM5C7IDJDU", - "name": "TestBatchingFolder", - "size": 0, - "createdBy": { - "application": { - "id": "de8bc8b5-d9f9-48b1-a8ad-b748da725064", - "displayName": "Graph explorer" - }, - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "application": { - "id": "de8bc8b5-d9f9-48b1-a8ad-b748da725064", - "displayName": "Graph explorer" - }, - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "fileSystemInfo": { - "createdDateTime": "2018-03-27T07:34:38Z", - "lastModifiedDateTime": "2018-03-27T07:34:38Z" - }, - "folder": { - "childCount": 0 - } - }, - { - "@odata.etag": "\"{DD092D3E-427F-45EA-8DAF-E25E7F77530C},3\"", - "id": "01BYE5RZZ6FUE5272C5JCY3L7CLZ7XOUYM", - "name": "All Japan Revenues By City.xlsx", - "size": 20051, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "I1snetixdavVoAQ0QMvy/W1dyhs=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:07:10Z", - "lastModifiedDateTime": "2017-08-07T16:07:10Z" - } - }, - { - "@odata.etag": "\"{5ADB5F85-9453-4E1D-889B-7A72E76E214C},4\"", - "id": "01BYE5RZ4FL7NVUU4UDVHIRG32OLTW4IKM", - "name": "Annual Financial Report (DRAFT).docx", - "size": 22750, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "YY1FIiSDCS9hcAptSPs7prNdf5A=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:01:51Z", - "lastModifiedDateTime": "2017-08-07T16:01:51Z" - } - }, - { - "@odata.etag": "\"{76967189-8511-4F0D-8747-647BF66A04C1},3\"", - "id": "01BYE5RZ4JOGLHMEMFBVHYOR3EPP3GUBGB", - "name": "Audit of Small Business Sales.xlsx", - "size": 21479, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "L1/Luatne7nMqtzVtacgJy9Mo24=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:02:35Z", - "lastModifiedDateTime": "2017-08-07T16:02:35Z" - } - }, - { - "@odata.etag": "\"{63D21C9F-FBBB-4FB0-A735-B7866AD6A169},4\"", - "id": "01BYE5RZ47DTJGHO73WBH2ONNXQZVNNILJ", - "name": "BrokenPipe.jpg", - "size": 5336, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/jpeg", - "hashes": { - "quickXorHash": "OGd+sgG4B/f1x6pn54GCsFTczzI=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:08:22Z", - "lastModifiedDateTime": "2017-08-07T16:08:22Z" - } - }, - { - "@odata.etag": "\"{A38DFFA7-801B-445F-8548-592A10378F63},2\"", - "id": "01BYE5RZ5H76G2GG4AL5CIKSCZFIIDPD3D", - "name": "Business Card.pdf", - "size": 866319, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/pdf", - "hashes": { - "quickXorHash": "dUiDqxP6UK9n5B2SZfHZmR6ZTCE=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-09-02T02:41:05Z", - "lastModifiedDateTime": "2017-09-02T02:41:05Z" - } - }, - { - "@odata.etag": "\"{68A26ADC-3C85-4EF5-AADD-C31091F963B2},4\"", - "id": "01BYE5RZ64NKRGRBJ46VHKVXODCCI7SY5S", - "name": "Contoso Patent App 150219a.docx", - "size": 86468, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "3nvnj/cnt17BRg9RNZVe0+xzEtA=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:39Z", - "lastModifiedDateTime": "2021-12-02T17:04:34Z" - } - }, - { - "@odata.etag": "\"{17A8BA57-138F-4CFA-B79F-1702C28BA88B},3\"", - "id": "01BYE5RZ2XXKUBPDYT7JGLPHYXALBIXKEL", - "name": "Contoso Patent Template.docx", - "size": 85596, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "jWK86kNVvULlV/oFKuGvDKybt+I=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:47Z", - "lastModifiedDateTime": "2017-08-07T16:03:47Z" - } - }, - { - "@odata.etag": "\"{3DF225B9-5E1F-402C-B8A4-131B81A9C0C9},2\"", - "id": "01BYE5RZ5ZEXZD2H26FRALRJATDOA2TQGJ", - "name": "Contoso Purchasing Data - Q1 - Copy.xlsx", - "size": 19334, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "R6C/c/IcimoRumUMtPxedGSVlI4=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:52Z", - "lastModifiedDateTime": "2017-08-07T16:03:52Z" - } - }, - { - "@odata.etag": "\"{11B8B053-851F-4382-B3B2-D61AAE13AB52},2\"", - "id": "01BYE5RZ2TWC4BCH4FQJB3HMWWDKXBHK2S", - "name": "Contoso Purchasing Data - Q1 KJ copy.xlsx", - "size": 21965, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "S/nt8zYOBaN6TTDTt7qzvGnV7hs=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:58Z", - "lastModifiedDateTime": "2017-08-07T16:03:58Z" - } - }, - { - "@odata.etag": "\"{C832384C-BAF8-471C-8FAA-8D0EE95A6951},3\"", - "id": "01BYE5RZ2MHAZMR6F2DRDY7KUNB3UVU2KR", - "name": "Contoso Purchasing Permissions - Confidential.docx", - "size": 100352, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "Xv81EyPRxiVr1EFrsA7p/k8SXQ8=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:04:25Z", - "lastModifiedDateTime": "2017-08-07T16:04:25Z" - } - }, - { - "@odata.etag": "\"{18FF008B-1D8D-4DC9-98F0-DFCCF93AE7EA},3\"", - "id": "01BYE5RZ4LAD7RRDI5ZFGZR4G7ZT4TVZ7K", - "name": "Contoso Purchasing Permissions - Q1.docx", - "size": 25268, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "2bRFLEN0C9368xMEyrCwSW9rrvg=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:04:17Z", - "lastModifiedDateTime": "2017-08-07T16:04:17Z" - } - }, - { - "@odata.etag": "\"{A0DD6857-DB76-4544-84E0-71DC1C411602},2\"", - "id": "01BYE5RZ2XNDO2A5W3IRCYJYDR3QOECFQC", - "name": "Employee Data - Q1 KJ copy.xlsx", - "size": 21693, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "XrTrOrxb+N4Yk93tWKLMs4fHGrs=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:04:31Z", - "lastModifiedDateTime": "2017-08-07T16:04:31Z" - } - }, - { - "@odata.etag": "\"{A1ED023A-C3A9-4BB3-AE5A-F0170251D3DC},3\"", - "id": "01BYE5RZZ2ALW2DKODWNF24WXQC4BFDU64", - "name": "Employee Health Accounts - Q3.xlsx", - "size": 21991, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "84xI2xs1lHJzsdTNGTfBdgcMRx4=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:09:15Z", - "lastModifiedDateTime": "2017-08-07T16:09:15Z" - } - }, - { - "@odata.etag": "\"{967FF8E3-A11C-4DBD-84CC-2CD067DF9290},2\"", - "id": "01BYE5RZ7D7B7ZMHFBXVGYJTBM2BT57EUQ", - "name": "Employee Health Accounts.xlsx", - "size": 21991, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "BmpX8QInJ+ysWNZE2sONhlgHrqY=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:04:44Z", - "lastModifiedDateTime": "2017-08-07T16:04:44Z" - } - }, - { - "@odata.etag": "\"{1219A7CA-6564-4C25-AB44-22FB95C24F69},2\"", - "id": "01BYE5RZ6KU4MREZDFEVGKWRBC7OK4ET3J", - "name": "Employee Travel - Q1 -KJ ONLY.xlsx", - "size": 21675, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "2prTvR+nOoKQF5MrrokHn+ZGVrE=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:04:50Z", - "lastModifiedDateTime": "2017-08-07T16:04:50Z" - } - }, - { - "@odata.etag": "\"{7A411B11-3CD1-4CC8-8346-B04866418C67},2\"", - "id": "01BYE5RZYRDNAXVUJ4ZBGIGRVQJBTEDDDH", - "name": "Employee Travel - Q1.xlsx", - "size": 21139, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "Bu4EDxQlhpb91ZqtA1gc5WdmKR0=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:05:01Z", - "lastModifiedDateTime": "2017-08-07T16:05:01Z" - } - }, - { - "@odata.etag": "\"{893DD5B3-BC7F-4657-AC41-B4870812833E},3\"", - "id": "01BYE5RZ5T2U6YS754K5DKYQNUQ4EBFAZ6", - "name": "Employee Travel - Q3.xlsx", - "size": 21147, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "Whh9gKTIUn2hCzgHV2jH5cExWAk=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:09:36Z", - "lastModifiedDateTime": "2017-08-07T16:09:36Z" - } - }, - { - "@odata.etag": "\"{8986F192-B784-4F8E-94A2-7613EDE08862},3\"", - "id": "01BYE5RZ4S6GDITBFXRZHZJITWCPW6BCDC", - "name": "European Expansion.pptx", - "size": 3578693, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "hashes": { - "quickXorHash": "roADpW29oFCSBnynYz7+1C2DvEw=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:06Z", - "lastModifiedDateTime": "2017-08-07T16:03:06Z" - } - }, - { - "@odata.etag": "\"{0BC35248-E4E2-4759-AD85-89407BCECCFE},4\"", - "id": "01BYE5RZ2IKLBQXYXELFD23BMJIB545TH6", - "name": "Fabrikam.one", - "size": 55782, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "displayName": "System Account" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/msonenote", - "hashes": { - "quickXorHash": "qAcnPQsI7iz4JQhSEBDhHAGYbGA=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:08:45Z", - "lastModifiedDateTime": "2020-01-09T03:02:37Z" - } - }, - { - "@odata.etag": "\"{D27D0780-9BD9-4D77-818D-8DA75253AC66},4\"", - "id": "01BYE5RZ4AA565FWM3O5GYDDMNU5JFHLDG", - "name": "Pricing Guidelines for XT1000.docx", - "size": 399606, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "oa/gT/YXhNSy6lJVgq94GDcfN+k=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:01:43Z", - "lastModifiedDateTime": "2017-08-07T16:01:43Z" - } - }, - { - "@odata.etag": "\"{21406A2F-1B59-4759-BCB9-B048CFC6E18D},2\"", - "id": "01BYE5RZZPNJACCWI3LFD3ZONQJDH4NYMN", - "name": "Projected Revenues Northwest and California.xlsx", - "size": 18411, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "+LIvYCfmz7zmWUJzWJyu9p8x5T4=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:12Z", - "lastModifiedDateTime": "2017-08-07T16:03:12Z" - } - }, - { - "@odata.etag": "\"{46F213BB-2549-469E-9F00-85AE5FB16C26},3\"", - "id": "01BYE5RZ53CPZEMSJFTZDJ6AEFVZP3C3BG", - "name": "Q3 Sales and Marketing Expense Report Audit.pptx", - "size": 1255076, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "hashes": { - "quickXorHash": "YvE2xflPfIMPeRk2qXhU6hgaa5s=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:23Z", - "lastModifiedDateTime": "2017-08-07T16:03:23Z" - } - }, - { - "@odata.etag": "\"{E02F6D14-5CD9-4FC7-AA33-49E80B967006},2\"", - "id": "01BYE5RZYUNUX6BWK4Y5H2UM2J5AFZM4AG", - "name": "RD And Engineering Costs Q1.xlsx", - "size": 16082, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "5b6EByk+4qZNtEBP2hgeoKAbXng=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:27Z", - "lastModifiedDateTime": "2017-08-07T16:03:27Z" - } - }, - { - "@odata.etag": "\"{108263D1-322A-48F0-94B2-D789646D9DB3},5\"", - "id": "01BYE5RZ6RMOBBAKRS6BEJJMWXRFSG3HNT", - "name": "RD Expense Report.pptx", - "size": 361107, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "hashes": { - "quickXorHash": "VakNQSFblGqCrIzBqYz1iQ4ePDM=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:07:45Z", - "lastModifiedDateTime": "2017-08-07T16:07:45Z" - } - }, - { - "@odata.etag": "\"{50AC12E6-7362-4AD8-B76B-E7D11F6B718F},2\"", - "id": "01BYE5RZ7GCKWFAYTT3BFLO27H2EPWW4MP", - "name": "RD Expenses Q1 to Q3.xlsx", - "size": 13394, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "52xvFlnNWV0Aa7ClVT3V9I4pzTE=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:03:31Z", - "lastModifiedDateTime": "2017-08-07T16:03:31Z" - } - }, - { - "@odata.etag": "\"{F257D0DA-DFB6-4F9B-8BA8-0045B89A3678},3\"", - "id": "01BYE5RZ622BL7FNW7TNHYXKAAIW4JUNTY", - "name": "Sales Invoice March.docx", - "size": 32467, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "suHG3yCwERG/1EDUqkkh/BUDkF8=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:05:08Z", - "lastModifiedDateTime": "2017-08-07T16:05:08Z" - } - }, - { - "@odata.etag": "\"{1AF92144-FC73-403A-81E1-F4707D9998C3},3\"", - "id": "01BYE5RZ2EEH4RU474HJAIDYPUOB6ZTGGD", - "name": "Sales Invoice Template.docx", - "size": 33755, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "f62p3sSBYK33J2PDRX/WNo0Q28Q=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:05:23Z", - "lastModifiedDateTime": "2017-08-07T16:05:23Z" - } - }, - { - "@odata.etag": "\"{3ED62E59-844B-4B3E-9A62-CD17666B1411},1\"", - "id": "01BYE5RZ2ZF3LD4S4EHZFZUYWNC5TGWFAR", - "name": "Temperatures.xlsx", - "size": 17498, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ56Y2GOVW7725BZO354PWSELRRZ", - "path": "/drive/root:", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "dLW5jkAKek0ZOTC8W2qlIQqHe1I=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-09-13T21:51:28Z", - "lastModifiedDateTime": "2017-09-13T21:51:28Z" - } - } - ] -} diff --git a/modules/storages/spec/support/payloads/specific_folder.json b/modules/storages/spec/support/payloads/specific_folder.json deleted file mode 100644 index b4b529ea599..00000000000 --- a/modules/storages/spec/support/payloads/specific_folder.json +++ /dev/null @@ -1,819 +0,0 @@ -{ - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives('b%21-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd')/items('01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY')/children(id,name,size,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference)", - "value": [ - { - "@odata.etag": "\"{29FEA8B3-FD38-4817-B672-5931A5CCAB77},3\"", - "id": "01BYE5RZ5TVD7CSOH5C5ELM4SZGGS4ZK3X", - "name": "00. Starfish Aquarium.jpg", - "size": 6497631, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/jpeg", - "hashes": { - "quickXorHash": "vs10ShaTW5v45eXVSTnEcoCkE1c=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:13:10Z", - "lastModifiedDateTime": "2017-08-07T16:13:10Z" - } - }, - { - "@odata.etag": "\"{DAEC05BC-7C34-4BDB-AAA3-8AE97F8CD16E},3\"", - "id": "01BYE5RZ54AXWNUND43NF2VI4K5F7YZULO", - "name": "01. Organic Chemistry Header Image.jpg", - "size": 21883, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/jpeg", - "hashes": { - "quickXorHash": "msdktpMpA7iz5NcDpEM+y+n1zbE=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:13:15Z", - "lastModifiedDateTime": "2017-08-07T16:13:15Z" - } - }, - { - "@odata.etag": "\"{6C93F38D-C92E-4A08-B809-CBF58DC85BD1},2\"", - "id": "01BYE5RZ4N6OJWYLWJBBFLQCOL6WG4QW6R", - "name": "02. Carbon at a glance.png", - "size": 208830, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "08DMBcdEuAZfqKhGQSQHBo4bCmE=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:13:21Z", - "lastModifiedDateTime": "2017-08-07T16:13:21Z" - } - }, - { - "@odata.etag": "\"{13F4CFAB-9AE0-457C-B864-4B349A04BC99},2\"", - "id": "01BYE5RZ5LZ72BHYE2PRC3QZCLGSNAJPEZ", - "name": "03. Valence Shell Theory.png", - "size": 435413, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "SjcS6NvWzaLRWB88F0SRMUYQ4UI=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:13:27Z", - "lastModifiedDateTime": "2017-08-07T16:13:27Z" - } - }, - { - "@odata.etag": "\"{BA5AA6FB-C2A9-4CCF-9181-678DB7648635},2\"", - "id": "01BYE5RZ73UZNLVKOCZ5GJDALHRW3WJBRV", - "name": "04. Lewis Dot Structures.png", - "size": 235196, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "LqboP8WgP2kMQRkQp+cCmeYSKCQ=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:13:34Z", - "lastModifiedDateTime": "2017-08-07T16:13:34Z" - } - }, - { - "@odata.etag": "\"{23ED8D10-23DB-4D77-B505-B601F5341761},2\"", - "id": "01BYE5RZYQRXWSHWZDO5G3KBNWAH2TIF3B", - "name": "05. Electron Orbital Shells.png", - "size": 435483, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "tn4os4CDsisRKVm6Vh9U5VIdXB4=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:13:39Z", - "lastModifiedDateTime": "2017-08-07T16:13:39Z" - } - }, - { - "@odata.etag": "\"{F983579B-3DB9-4615-99E1-3CDBE5E461FB},2\"", - "id": "01BYE5RZ43K6B7TOJ5CVDJTYJ43PS6IYP3", - "name": "06. Methane.png", - "size": 409225, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "Eowf9244MlcbFX8kJgalbZ6G1sY=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:10:24Z", - "lastModifiedDateTime": "2017-08-07T16:10:24Z" - } - }, - { - "@odata.etag": "\"{1D11838A-6192-4672-8184-8B088F2F15D0},2\"", - "id": "01BYE5RZ4KQMIR3ETBOJDIDBELBCHS6FOQ", - "name": "07. Propane.png", - "size": 306231, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "w+9jiYGr5aN9Q9ru9xlPZK7oU1k=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:10:30Z", - "lastModifiedDateTime": "2017-08-07T16:10:30Z" - } - }, - { - "@odata.etag": "\"{F17679AC-6061-472A-A796-EB11FC90DA82},2\"", - "id": "01BYE5RZ5MPF3PCYLAFJD2PFXLCH6JBWUC", - "name": "08. Carbon Compound Table.png", - "size": 14597, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "oLCUpTXd32yf6liw5j/MbR4Wvxk=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:10:36Z", - "lastModifiedDateTime": "2017-08-07T16:10:36Z" - } - }, - { - "@odata.etag": "\"{825DD868-AD74-4935-9817-8F496E819B97},2\"", - "id": "01BYE5RZ3I3BOYE5FNGVEZQF4PJFXIDG4X", - "name": "09. Variety of Carbon Compounds.png", - "size": 22672, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "tehxVPeglRGSDI8nKDNp0K39eik=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:10:49Z", - "lastModifiedDateTime": "2017-08-07T16:10:49Z" - } - }, - { - "@odata.etag": "\"{7990C2BA-AB09-427A-B293-2767FB7B193A},2\"", - "id": "01BYE5RZ52YKIHSCNLPJBLFEZHM75XWGJ2", - "name": "10. Carbon Decay.png", - "size": 74625, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "LnW73ofTqASJWA92x1HTkLVOFGs=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:11:17Z", - "lastModifiedDateTime": "2017-08-07T16:11:17Z" - } - }, - { - "@odata.etag": "\"{6B3B123C-FC6C-4319-A76A-1F31D99ACC1C},2\"", - "id": "01BYE5RZZ4CI5WW3H4DFB2O2Q7GHMZVTA4", - "name": "11. Bent Line.png", - "size": 3512, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "KmXdkwTJJFS4Uvok/5udpmgzUdc=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:11:32Z", - "lastModifiedDateTime": "2017-08-07T16:11:32Z" - } - }, - { - "@odata.etag": "\"{94411F86-B6B8-4DA1-82D8-62ECF1E2B243},2\"", - "id": "01BYE5RZ4GD5AZJOFWUFGYFWDC5TY6FMSD", - "name": "12. Carbon Bent Line.png", - "size": 10368, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "rkHvqD3shTWiQjOJ8+pnpQR/mv0=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:11:47Z", - "lastModifiedDateTime": "2017-08-07T16:11:47Z" - } - }, - { - "@odata.etag": "\"{AE9D0F42-1C7F-466E-ABBF-53A364918D80},2\"", - "id": "01BYE5RZ2CB6O247Y4NZDKXP2TUNSJDDMA", - "name": "13. Carbon Bent Line with Hydrogen.png", - "size": 15680, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "image/png", - "hashes": { - "quickXorHash": "6jr1aC+d8rQ8Lrk0YvTy6niAfZU=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:11:53Z", - "lastModifiedDateTime": "2017-08-07T16:11:53Z" - } - }, - { - "@odata.etag": "\"{CA1A75CC-8500-468F-A1E5-8FB18FBE5174},3\"", - "id": "01BYE5RZ6MOUNMUAEFR5DKDZMPWGH34ULU", - "name": "Carbon Bonding Presentation.pptx", - "size": 4537346, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "hashes": { - "quickXorHash": "KO255SpJizCWdbezZO9XCXnVDB8=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:12:03Z", - "lastModifiedDateTime": "2017-08-07T16:12:03Z" - } - }, - { - "@odata.etag": "\"{2964723D-9E45-470E-8FE4-85CEDA9D4018},9\"", - "id": "01BYE5RZZ5OJSCSRM6BZDY7ZEFZ3NJ2QAY", - "name": "Carbon Deposits Analysis.xlsx", - "size": 1137258, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "ehfW2RDG8vH21b1TWr71vW7VK3I=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:12:10Z", - "lastModifiedDateTime": "2017-09-14T23:52:31Z" - } - }, - { - "@odata.etag": "\"{83832B8C-D37A-4685-BE7D-508A6BFEA005},2\"", - "id": "01BYE5RZ4MFOBYG6WTQVDL47KQRJV75IAF", - "name": "Carbon_Analysis_DB.xlsx", - "size": 171839, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "hashes": { - "quickXorHash": "dk0lSPG8jksFFZ83haivn9/rgSA=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:12:16Z", - "lastModifiedDateTime": "2017-08-07T16:12:16Z" - } - }, - { - "@odata.etag": "\"{01CFFE3D-8938-40A1-A002-780756B3F133},2\"", - "id": "01BYE5RZZ573HQCOEJUFAKAATYA5LLH4JT", - "name": "Figure_2._Energy-related_carbon_dioxide_emissions_by_fuel_1990-2014.csv", - "size": 1011, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.ms-excel", - "hashes": { - "quickXorHash": "E/2wFtgXpG6xRz6tSbN8kBOpVAI=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:12:26Z", - "lastModifiedDateTime": "2017-08-07T16:12:26Z" - } - }, - { - "@odata.etag": "\"{49F57DF2-EBC1-4814-B0B9-5774FCFBD00F},3\"", - "id": "01BYE5RZ7SPX2UTQPLCRELBOKXOT6PXUAP", - "name": "Intro to Chemistry.docx", - "size": 23618, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "5iU/hQNZ1+jpeWxh1rHBc/9wC8M=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:12:31Z", - "lastModifiedDateTime": "2017-08-07T16:12:31Z" - } - }, - { - "@odata.etag": "\"{4898251F-53A8-4B6A-B48D-015B15EAA79D},2\"", - "id": "01BYE5RZY7EWMERKCTNJF3JDIBLMK6VJ45", - "name": "Introduction to Carbon Bonding Supplemental.pdf", - "size": 89385, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/pdf", - "hashes": { - "quickXorHash": "im49DsSfyErHtQ9+9zJTYejcjWk=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:12:36Z", - "lastModifiedDateTime": "2017-08-07T16:12:36Z" - } - }, - { - "@odata.etag": "\"{FB0920BF-A14A-42CE-BB19-8501D66E478E},3\"", - "id": "01BYE5RZ57EAE7WSVBZZBLWGMFAHLG4R4O", - "name": "Introduction to Carbon Bonding.docx", - "size": 1494485, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "0+jreQHoNBpOn3/AheF7etZprko=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:12:43Z", - "lastModifiedDateTime": "2017-08-07T16:12:43Z" - } - }, - { - "@odata.etag": "\"{CB286026-5932-4530-BA0C-A7FA8C7313A4},3\"", - "id": "01BYE5RZZGMAUMWMSZGBC3UDFH7KGHGE5E", - "name": "Molecular Diagram.docx", - "size": 505504, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZYJ43UXGBP23BBIFPISHHMCDTOY", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Class Documents", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "PqgLlywWTeLXbY6LRoYwvarjc5I=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:13:00Z", - "lastModifiedDateTime": "2017-08-07T16:13:00Z" - } - } - ] -} diff --git a/modules/storages/spec/support/payloads/two_levels_deep_folder.json b/modules/storages/spec/support/payloads/two_levels_deep_folder.json deleted file mode 100644 index 0ad2dbdce38..00000000000 --- a/modules/storages/spec/support/payloads/two_levels_deep_folder.json +++ /dev/null @@ -1,1152 +0,0 @@ -{ - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives('b%21-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd')/items('01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN')/children(id,name,size,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference)", - "value": [ - { - "@odata.etag": "\"{DCDAF38F-91AE-42A0-B915-D3576FEE79B3},3\"", - "id": "01BYE5RZ4P6PNNZLURUBBLSFOTK5X646NT", - "name": "2014 Grammys.docx", - "size": 23958, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "d0BNg8r7dTdlY0/N+arjDiMs2qE=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:11Z", - "lastModifiedDateTime": "2017-08-07T16:20:11Z" - } - }, - { - "@odata.etag": "\"{40DBFE82-CB2F-43BF-9529-2A35C045ADA0},3\"", - "id": "01BYE5RZ4C73NUAL6LX5BZKKJKGXAELLNA", - "name": "2015 Emmys.docx", - "size": 23961, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "rAN0bLHu5KlsFtOCR4vDjPrIvaI=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:51Z", - "lastModifiedDateTime": "2017-08-07T16:22:51Z" - } - }, - { - "@odata.etag": "\"{996EC21D-F22A-41C4-B1D8-6594F6172D5E},4\"", - "id": "01BYE5RZY5YJXJSKXSYRA3DWDFST3BOLK6", - "name": "2015 VMAs.docx", - "size": 23464, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "ghOFlj94+mJ66N5lueUsYmpQpDc=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:23:03Z", - "lastModifiedDateTime": "2017-08-07T16:23:03Z" - } - }, - { - "@odata.etag": "\"{334663A3-CD4D-4D94-9F2C-A82BDEFAD869},3\"", - "id": "01BYE5RZ5DMNDDGTONSRGZ6LFIFPPPVWDJ", - "name": "2016 Emmys.docx", - "size": 23946, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "4HdGLZY9wp7LnoepH6HNl9HIwhI=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:48Z", - "lastModifiedDateTime": "2017-08-07T16:21:48Z" - } - }, - { - "@odata.etag": "\"{69E164C3-9DCF-48AB-97BA-A3A72EE0A633},3\"", - "id": "01BYE5RZ6DMTQWTT45VNEJPOVDU4XOBJRT", - "name": "2016 Grammys.docx", - "size": 23466, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "WG+V5e6JdfoOrR5JyGnf5HVhgvc=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:12Z", - "lastModifiedDateTime": "2017-08-07T16:22:12Z" - } - }, - { - "@odata.etag": "\"{20AC32DF-E9C2-42DE-9A3C-D0F8CDB2E677},3\"", - "id": "01BYE5RZ67GKWCBQXJ3ZBJUPGQ7DG3FZTX", - "name": "2016 Tonys.docx", - "size": 23468, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "EST/qgVPct6PTc6qh0Z5SgdkJxk=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:54Z", - "lastModifiedDateTime": "2017-08-07T16:21:54Z" - } - }, - { - "@odata.etag": "\"{F1DDC4BF-6CB6-4210-AC33-0E0770D261E6},3\"", - "id": "01BYE5RZ57YTO7DNTMCBBKYMYOA5YNEYPG", - "name": "Anna Tuskova.docx", - "size": 23461, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "6W4NGpBe5SEKOoomp95ywv0SCkM=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:23:11Z", - "lastModifiedDateTime": "2017-08-07T16:23:11Z" - } - }, - { - "@odata.etag": "\"{1BCFC5D0-35E3-497C-8979-649FE1CEC42F},4\"", - "id": "01BYE5RZ6QYXHRXYZVPREYS6LET7Q45RBP", - "name": "Chinese fashion at scale.docx", - "size": 23445, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "jPxc6Dk61HVCpdJZxJwFX1LYDpA=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:26Z", - "lastModifiedDateTime": "2017-08-07T16:21:26Z" - } - }, - { - "@odata.etag": "\"{607C5720-E40D-4C01-9610-DEF147B82951},3\"", - "id": "01BYE5RZZAK56GADPEAFGJMEG66FD3QKKR", - "name": "Christian Siriano.docx", - "size": 23460, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "wfmjZJzgziEOKlvVludUyuuBJTI=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:20Z", - "lastModifiedDateTime": "2017-08-07T16:21:20Z" - } - }, - { - "@odata.etag": "\"{C19FF8F9-4F4B-4428-B6D0-8FC20F9BB020},3\"", - "id": "01BYE5RZ7Z7CP4CS2PFBCLNUEPYIHZXMBA", - "name": "Eco-logical fashions.docx", - "size": 23458, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "0b2GDSDgFuVep/f8KCEsCWAPOVQ=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:24Z", - "lastModifiedDateTime": "2017-08-07T16:20:24Z" - } - }, - { - "@odata.etag": "\"{53221BF3-B74B-4746-8A33-2841DA421FE5},3\"", - "id": "01BYE5RZ7TDMRFGS5XIZDYUMZIIHNEEH7F", - "name": "Fashion Comebacks.docx", - "size": 23455, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "5e8eo9ib8YQ08hV86md9l0Ove5s=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:46Z", - "lastModifiedDateTime": "2017-08-07T16:20:46Z" - } - }, - { - "@odata.etag": "\"{96328E72-852A-4E7F-8E50-8223F7920349},3\"", - "id": "01BYE5RZ3SRYZJMKUFP5HI4UECEP3ZEA2J", - "name": "Fashion faux pas.docx", - "size": 21285, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "11rHdNtmU3+yw4WGkXXRP6oZSxM=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:03Z", - "lastModifiedDateTime": "2017-08-07T16:21:03Z" - } - }, - { - "@odata.etag": "\"{0CD091C2-C88D-446B-B6EE-99A267072C19},3\"", - "id": "01BYE5RZ6CSHIAZDOINNCLN3UZUJTQOLAZ", - "name": "Fashion Hall of Fame.docx", - "size": 23461, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "W3DuHZ8gTKA1BWECpkvnDxoLHA0=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:58Z", - "lastModifiedDateTime": "2017-08-07T16:22:58Z" - } - }, - { - "@odata.etag": "\"{C3B5618A-40EA-4B15-AE09-509EA79900A6},3\"", - "id": "01BYE5RZ4KMG24H2SACVF24CKQT2TZSAFG", - "name": "From board room to barre.docx", - "size": 23462, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "dmgV9QESbG8kyHlAJ5axHVDIS2Q=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:46Z", - "lastModifiedDateTime": "2017-08-07T16:22:46Z" - } - }, - { - "@odata.etag": "\"{F3219E6E-2123-4B26-A82B-C7DF945AAA58},3\"", - "id": "01BYE5RZ3OTYQ7GIZBEZF2QK6H36KFVKSY", - "name": "Gala benefit unites designers.docx", - "size": 21278, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "YNEPx0KpvAY1cq10OzmcrcU6JNU=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:08Z", - "lastModifiedDateTime": "2017-08-07T16:21:08Z" - } - }, - { - "@odata.etag": "\"{0636EF27-44AF-41C1-B67B-EA07A1BBAA6F},3\"", - "id": "01BYE5RZZH543ANL2EYFA3M67KA6Q3XKTP", - "name": "Hillary and Donald fashion review.docx", - "size": 23460, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "WT6wqzpqdTnvex8VV8O8ijaL73k=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:14Z", - "lastModifiedDateTime": "2017-08-07T16:21:14Z" - } - }, - { - "@odata.etag": "\"{48FBAA75-783F-47A8-B74A-4428D6A50391},3\"", - "id": "01BYE5RZ3VVL5UQP3YVBD3OSSEFDLKKA4R", - "name": "Indigenous ingenuity.docx", - "size": 21267, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "wtYRBlARBSO8x9hxnINr1/bjd9A=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:18Z", - "lastModifiedDateTime": "2017-08-07T16:20:18Z" - } - }, - { - "@odata.etag": "\"{57D4603B-6298-42D1-B37D-35A799AE9240},3\"", - "id": "01BYE5RZZ3MDKFPGDC2FBLG7JVU6M25ESA", - "name": "La Formela.docx", - "size": 23452, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "22DhA0aMTMRFIgZ4fmU1+ycktHY=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:35Z", - "lastModifiedDateTime": "2017-08-07T16:20:35Z" - } - }, - { - "@odata.etag": "\"{6881B236-072A-43D3-AFCE-88A6AD84CD40},4\"", - "id": "01BYE5RZZWWKAWQKQH2NB27TUIU2WYJTKA", - "name": "Martina Spelova.docx", - "size": 23461, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "LWSLZvihkpblNPT3hghSgHGjIm0=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:40Z", - "lastModifiedDateTime": "2017-08-07T16:20:40Z" - } - }, - { - "@odata.etag": "\"{942BFD9A-014E-4980-8DCF-76E4F3995FCE},4\"", - "id": "01BYE5RZ427UVZITQBQBEY3T3W4TZZSX6O", - "name": "Michael Kors.docx", - "size": 23478, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "g4ukxdbTWUHVpxFCMEumQJKSY38=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:17Z", - "lastModifiedDateTime": "2017-08-07T16:22:17Z" - } - }, - { - "@odata.etag": "\"{3AF2BB6C-4EB4-45DF-A767-C55D23CAAFA7},3\"", - "id": "01BYE5RZ3MXPZDVNCO35C2OZ6FLUR4VL5H", - "name": "Naturally beautiful.docx", - "size": 23449, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "b/PssHn764TT7bkvWZRLNnUcG+g=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:32Z", - "lastModifiedDateTime": "2017-08-07T16:21:32Z" - } - }, - { - "@odata.etag": "\"{A633F82D-34A5-49D3-8A10-29578EFF6F3E},3\"", - "id": "01BYE5RZZN7AZ2NJJU2NEYUEBJK6HP63Z6", - "name": "Odivi.docx", - "size": 23455, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "Gt5+Bv41Xa+8sJktYKp8VGkiHDo=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:34Z", - "lastModifiedDateTime": "2017-08-07T16:22:34Z" - } - }, - { - "@odata.etag": "\"{7E7F79EA-A033-4A8E-92D4-61419FAE7B6A},3\"", - "id": "01BYE5RZ7KPF7X4M5ARZFJFVDBIGP2463K", - "name": "Oh no she didn't!.docx", - "size": 21263, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "uCAU3jzt8VUjTSCM4tvDj91CFjQ=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:51Z", - "lastModifiedDateTime": "2017-08-07T16:20:51Z" - } - }, - { - "@odata.etag": "\"{F35F5F78-D03C-43FE-9442-569BCBC21C05},4\"", - "id": "01BYE5RZ3YL5P7GPGQ7ZBZIQSWTPF4EHAF", - "name": "Plus size sizzles at NYFW.docx", - "size": 23458, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "TBulJkOYsqi+KYA7ecy+6qOjvLA=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:25Z", - "lastModifiedDateTime": "2017-08-07T16:22:25Z" - } - }, - { - "@odata.etag": "\"{B186713A-069F-4014-857D-94C3E518CA1A},3\"", - "id": "01BYE5RZZ2OGDLDHYGCRAIK7MUYPSRRSQ2", - "name": "Stand-out trends from NYFW.docx", - "size": 23448, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "yWPj2p4i1wpSQLFhOwjz3KUZR+4=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:38Z", - "lastModifiedDateTime": "2017-08-07T16:21:38Z" - } - }, - { - "@odata.etag": "\"{77321535-2D7D-45CC-B452-B44C769E9840},3\"", - "id": "01BYE5RZZVCUZHO7JNZRC3IUVUJR3J5GCA", - "name": "Standouts on the London Runway.docx", - "size": 23452, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "6Mxq88vBvs3cWB8HGMm4yOnf9g4=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:23:17Z", - "lastModifiedDateTime": "2017-08-07T16:23:17Z" - } - }, - { - "@odata.etag": "\"{3472AFB6-FD8A-4DBC-A1D3-741679483D02},3\"", - "id": "01BYE5RZ5WV5ZDJCX5XRG2DU3UCZ4UQPIC", - "name": "Style and substance.docx", - "size": 23456, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "igch7Uf94GVSqQCW+MwuNvy4rL4=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:04Z", - "lastModifiedDateTime": "2017-08-07T16:22:04Z" - } - }, - { - "@odata.etag": "\"{A2968BFC-5AE7-4E98-8339-BE3DB39ABC5E},3\"", - "id": "01BYE5RZ74ROLKFZ22TBHIGON6HWZZVPC6", - "name": "Technology and Fashion.docx", - "size": 23448, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "b8hxHDyma2y5PNIMkj/KhTJ2QTQ=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:05Z", - "lastModifiedDateTime": "2017-08-07T16:20:05Z" - } - }, - { - "@odata.etag": "\"{462CFCEA-A8A0-459B-B3EE-C70CBC497960},3\"", - "id": "01BYE5RZ7K7QWENIFITNC3H3WHBS6ES6LA", - "name": "Wearing it well.docx", - "size": 23449, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "cd6bUWVx8UksY5At9lwvVA0D72Q=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:20:30Z", - "lastModifiedDateTime": "2017-08-07T16:20:30Z" - } - }, - { - "@odata.etag": "\"{559B5B0A-BD29-4483-87A5-0AD62112B2E7},3\"", - "id": "01BYE5RZYKLONVKKN5QNCIPJIK2YQRFMXH", - "name": "Wedding day wonderful.docx", - "size": 23446, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "/zcsEWB1rvbqgxWK53mtREHFVHg=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:22:00Z", - "lastModifiedDateTime": "2017-08-07T16:22:00Z" - } - }, - { - "@odata.etag": "\"{652C283F-CB8C-4114-A95D-B50BE3E979BD},4\"", - "id": "01BYE5RZZ7FAWGLDGLCRA2SXNVBPR6S6N5", - "name": "Where are they now.docx", - "size": 23454, - "createdBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "lastModifiedBy": { - "user": { - "email": "MeganB@M365x214355.onmicrosoft.com", - "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", - "displayName": "Megan Bowen" - } - }, - "parentReference": { - "driveType": "business", - "driveId": "b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd", - "id": "01BYE5RZ2LCUJQWLOZYNDLDC4DRRGJ7OEN", - "path": "/drives/b!-RIj2DuyvEyV1T4NlOaMHk8XkS_I8MdFlUCq1BlcjgmhRfAj3-Z8RY2VpuvV_tpd/root:/Contoso Clothing/Clothing Articles", - "siteId": "d82312f9-b23b-4cbc-95d5-3e0d94e68c1e" - }, - "file": { - "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "hashes": { - "quickXorHash": "5KPM5/zH1WRbJN8iVSwbgILx1TE=" - } - }, - "fileSystemInfo": { - "createdDateTime": "2017-08-07T16:21:43Z", - "lastModifiedDateTime": "2017-08-07T16:21:43Z" - } - } - ] -} diff --git a/modules/storages/spec/support/shared_examples_for_adapters/create_folder_command_examples.rb b/modules/storages/spec/support/shared_examples_for_storage_providers/create_folder_command_examples.rb similarity index 62% rename from modules/storages/spec/support/shared_examples_for_adapters/create_folder_command_examples.rb rename to modules/storages/spec/support/shared_examples_for_storage_providers/create_folder_command_examples.rb index 7c68f2cc762..5132d2f88d4 100644 --- a/modules/storages/spec/support/shared_examples_for_adapters/create_folder_command_examples.rb +++ b/modules/storages/spec/support/shared_examples_for_storage_providers/create_folder_command_examples.rb @@ -28,31 +28,27 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples_for "create_folder_command: basic command setup" do +RSpec.shared_examples_for "adapter create_folder_command: basic command setup" do it "is registered as commands.create_folder" do - expect(Storages::Peripherals::Registry - .resolve("#{storage}.commands.create_folder")).to eq(described_class) + expect(Storages::Adapters::Registry.resolve("#{storage}.commands.create_folder")).to eq(described_class) end it "responds to #call with correct parameters" do expect(described_class).to respond_to(:call) method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq folder_name], - %i[keyreq parent_location]) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) end end -RSpec.shared_examples_for "create_folder_command: successful folder creation" do +RSpec.shared_examples_for "adapter create_folder_command: successful folder creation" do it "creates a folder" do - result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success - response = result.result - expect(response).to be_a(Storages::StorageFile) + response = result.value! + expect(response).to be_a(Storages::Adapters::Results::StorageFile) expect(response.name).to eq(folder_name) expect(response.location).to eq(path) ensure @@ -60,26 +56,26 @@ RSpec.shared_examples_for "create_folder_command: successful folder creation" do end end -RSpec.shared_examples_for "create_folder_command: parent not found" do +RSpec.shared_examples_for "adapter create_folder_command: parent not found" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:not_found) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(described_class) end end -RSpec.shared_examples_for "create_folder_command: folder already exists" do +RSpec.shared_examples_for "adapter create_folder_command: folder already exists" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, folder_name:, parent_location:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:conflict) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(described_class) end end diff --git a/modules/storages/spec/support/shared_examples_for_adapters/file_info_query_examples.rb b/modules/storages/spec/support/shared_examples_for_storage_providers/file_info_query_examples.rb similarity index 66% rename from modules/storages/spec/support/shared_examples_for_adapters/file_info_query_examples.rb rename to modules/storages/spec/support/shared_examples_for_storage_providers/file_info_query_examples.rb index df8cbe493e4..7c0a9aff99e 100644 --- a/modules/storages/spec/support/shared_examples_for_adapters/file_info_query_examples.rb +++ b/modules/storages/spec/support/shared_examples_for_storage_providers/file_info_query_examples.rb @@ -28,9 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples_for "file_info_query: basic query setup" do +RSpec.shared_examples_for "adapter file_info_query: basic query setup" do it "is registered as queries.file_info" do - expect(Storages::Peripherals::Registry + expect(Storages::Adapters::Registry .resolve("#{storage}.queries.file_info")).to eq(described_class) end @@ -40,49 +40,42 @@ RSpec.shared_examples_for "file_info_query: basic query setup" do method = described_class.method(:call) expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], - %i[keyreq file_id]) + %i[keyreq input_data]) end end -RSpec.shared_examples_for "file_info_query: successful file/folder response" do +RSpec.shared_examples_for "adapter file_info_query: successful file/folder response" do it "returns a file info object" do - result = described_class.call(storage:, auth_strategy:, file_id:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success - response = result.result - expect(response).to be_a(Storages::StorageFileInfo) + response = result.value! + expect(response).to be_a(Storages::Adapters::Results::StorageFileInfo) expect(response).to eq(file_info) end end -RSpec.shared_examples_for "file_info_query: not found" do +RSpec.shared_examples_for "adapter file_info_query: not found" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, file_id:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:not_found) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(error_source) end end -RSpec.shared_examples_for "file_info_query: error" do +RSpec.shared_examples_for "adapter file_info_query: error" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, file_id:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:error) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(described_class) end end - -RSpec.shared_examples_for "file_info_query: validating input data" do - let(:file_id) { nil } - let(:error_source) { described_class } - - it_behaves_like "file_info_query: error" -end diff --git a/modules/storages/spec/support/shared_examples_for_adapters/file_path_to_id_map_query_examples.rb b/modules/storages/spec/support/shared_examples_for_storage_providers/file_path_to_id_map_query_examples.rb similarity index 71% rename from modules/storages/spec/support/shared_examples_for_adapters/file_path_to_id_map_query_examples.rb rename to modules/storages/spec/support/shared_examples_for_storage_providers/file_path_to_id_map_query_examples.rb index 6d84b31c2a7..93631fb321d 100644 --- a/modules/storages/spec/support/shared_examples_for_adapters/file_path_to_id_map_query_examples.rb +++ b/modules/storages/spec/support/shared_examples_for_storage_providers/file_path_to_id_map_query_examples.rb @@ -28,9 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples_for "file_path_to_id_map_query: basic query setup" do +RSpec.shared_examples_for "adapter file_path_to_id_map_query: basic query setup" do it "is registered as queries.file_path_to_id_map" do - expect(Storages::Peripherals::Registry + expect(Storages::Adapters::Registry .resolve("#{storage}.queries.file_path_to_id_map")).to eq(described_class) end @@ -40,34 +40,28 @@ RSpec.shared_examples_for "file_path_to_id_map_query: basic query setup" do method = described_class.method(:call) expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], - %i[keyreq folder], - %i[key depth]) + %i[keyreq input_data]) end end -RSpec.shared_examples_for "file_path_to_id_map_query: successful query" do +RSpec.shared_examples_for "adapter file_path_to_id_map_query: successful query" do it "returns a map of locations to file ids" do - result = if defined?(depth) - described_class.call(storage:, auth_strategy:, folder:, depth:) - else - described_class.call(storage:, auth_strategy:, folder:) - end - + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success - response = result.result + response = result.value! expect(response.transform_values(&:id)).to eq(expected_ids) end end -RSpec.shared_examples_for "file_path_to_id_map_query: not found" do +RSpec.shared_examples_for "adapter file_path_to_id_map_query: not found" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, folder:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:not_found) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(error_source) end end diff --git a/modules/storages/spec/support/shared_examples_for_adapters/files_query_examples.rb b/modules/storages/spec/support/shared_examples_for_storage_providers/files_query_examples.rb similarity index 61% rename from modules/storages/spec/support/shared_examples_for_adapters/files_query_examples.rb rename to modules/storages/spec/support/shared_examples_for_storage_providers/files_query_examples.rb index 17a15629427..784d328db4b 100644 --- a/modules/storages/spec/support/shared_examples_for_adapters/files_query_examples.rb +++ b/modules/storages/spec/support/shared_examples_for_storage_providers/files_query_examples.rb @@ -28,10 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples_for "files_query: basic query setup" do +RSpec.shared_examples_for "adapter files_query: basic query setup" do it "is registered as queries.files" do - expect(Storages::Peripherals::Registry - .resolve("#{storage}.queries.files")).to eq(described_class) + expect(Storages::Adapters::Registry.resolve("#{storage}.queries.files")).to eq(described_class) end it "responds to #call with correct parameters" do @@ -40,58 +39,42 @@ RSpec.shared_examples_for "files_query: basic query setup" do method = described_class.method(:call) expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], - %i[keyreq folder]) + %i[keyreq input_data]) end end -RSpec.shared_examples_for "files_query: successful files response" do +RSpec.shared_examples_for "adapter files_query: successful files response" do it "returns a file info object" do - result = described_class.call(storage:, auth_strategy:, folder:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success - response = result.result - expect(response).to be_a(Storages::StorageFiles) + response = result.value! + expect(response).to be_a(Storages::Adapters::Results::StorageFileCollection) expect(response).to eq(files_result) end end -RSpec.shared_examples_for "files_query: not found" do +RSpec.shared_examples_for "adapter files_query: not found" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, folder:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:not_found) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(described_class) end end -RSpec.shared_examples_for "files_query: error" do +RSpec.shared_examples_for "adapter files_query: error" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, folder:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:error) - expect(error.data.source).to eq(error_source) - end -end - -RSpec.shared_examples_for "files_query: validating input data" do - let(:error_source) { described_class } - - context "if folder is no parent folder object" do - let(:folder) { "/root" } - - it_behaves_like "files_query: error" - end - - context "if folder path is nil" do - let(:folder) { nil } - - it_behaves_like "files_query: error" + expect(error.source).to eq(described_class) end end diff --git a/modules/storages/spec/support/shared_examples_for_adapters/rename_file_command_examples.rb b/modules/storages/spec/support/shared_examples_for_storage_providers/rename_file_command_examples.rb similarity index 56% rename from modules/storages/spec/support/shared_examples_for_adapters/rename_file_command_examples.rb rename to modules/storages/spec/support/shared_examples_for_storage_providers/rename_file_command_examples.rb index 1f71426f20c..48557d9171f 100644 --- a/modules/storages/spec/support/shared_examples_for_adapters/rename_file_command_examples.rb +++ b/modules/storages/spec/support/shared_examples_for_storage_providers/rename_file_command_examples.rb @@ -28,74 +28,52 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples_for "rename_file_command: basic command setup" do +RSpec.shared_examples_for "adapter rename_file_command: basic command setup" do it "is registered as commands.rename_file" do - expect(Storages::Peripherals::Registry - .resolve("#{storage}.commands.rename_file")).to eq(described_class) + expect(Storages::Adapters::Registry.resolve("#{storage}.commands.rename_file")).to eq(described_class) end it "responds to #call with correct parameters" do expect(described_class).to respond_to(:call) method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq file_id], - %i[keyreq name]) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) end end -RSpec.shared_examples_for "rename_file_command: successful file renaming" do +RSpec.shared_examples_for "adapter rename_file_command: successful file renaming" do it "returns success and the renamed file" do - result = described_class.call(storage:, auth_strategy:, file_id:, name:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success - response = result.result - expect(response).to be_a(Storages::StorageFile) + response = result.value! + expect(response).to be_a(Storages::Adapters::Results::StorageFile) expect(response.id).to eq(file_id) expect(response.name).to eq(name) end end -RSpec.shared_examples_for "rename_file_command: not found" do +RSpec.shared_examples_for "adapter rename_file_command: not found" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, file_id:, name:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:not_found) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(error_source || described_class) end end -RSpec.shared_examples_for "rename_file_command: error" do +RSpec.shared_examples_for "adapter rename_file_command: error" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, file_id:, name:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:error) - expect(error.data.source).to eq(error_source) - end -end - -RSpec.shared_examples_for "rename_file_command: validating input data" do - let(:error_source) { described_class } - let(:file_id) { "my_file" } - let(:name) { "new_name" } - - context "if file_id is empty" do - let(:file_id) { "" } - - it_behaves_like "rename_file_command: error" - end - - context "if name is empty" do - let(:name) { "" } - - it_behaves_like "rename_file_command: error" + expect(error.source).to eq(described_class) end end diff --git a/modules/storages/spec/support/shared_examples_for_adapters/set_permissions_command_examples.rb b/modules/storages/spec/support/shared_examples_for_storage_providers/set_permissions_command_examples.rb similarity index 65% rename from modules/storages/spec/support/shared_examples_for_adapters/set_permissions_command_examples.rb rename to modules/storages/spec/support/shared_examples_for_storage_providers/set_permissions_command_examples.rb index 2e423ea664e..4d9de4cd999 100644 --- a/modules/storages/spec/support/shared_examples_for_adapters/set_permissions_command_examples.rb +++ b/modules/storages/spec/support/shared_examples_for_storage_providers/set_permissions_command_examples.rb @@ -28,9 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples_for "set_permissions_command: basic command setup" do +RSpec.shared_examples_for "adapter set_permissions_command: basic command setup" do it "is registered as commands.set_permissions" do - expect(Storages::Peripherals::Registry + expect(Storages::Adapters::Registry .resolve("#{storage}.commands.set_permissions")).to eq(described_class) end @@ -38,27 +38,20 @@ RSpec.shared_examples_for "set_permissions_command: basic command setup" do expect(described_class).to respond_to(:call) method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq input_data]) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) end end -RSpec.shared_examples_for "set_permissions_command: replaces already set permissions" do +RSpec.shared_examples_for "adapter set_permissions_command: replaces already set permissions" do it "replaces fully the previously set permissions" do file_id = test_folder.id - - input_data = Storages::Peripherals::StorageInteraction::Inputs::SetPermissions - .build(file_id:, user_permissions: previous_permissions) - .value! + input_data = Storages::Adapters::Input::SetPermissions.build(file_id:, user_permissions: previous_permissions).value! result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success expect(current_remote_permissions).to eq(previous_permissions) - input_data = Storages::Peripherals::StorageInteraction::Inputs::SetPermissions - .build(file_id:, user_permissions: replacing_permissions) - .value! + input_data = Storages::Adapters::Input::SetPermissions.build(file_id:, user_permissions: replacing_permissions).value! result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success @@ -68,15 +61,13 @@ RSpec.shared_examples_for "set_permissions_command: replaces already set permiss end end -RSpec.shared_examples_for "set_permissions_command: creates new permissions" do +RSpec.shared_examples_for "adapter set_permissions_command: creates new permissions" do it "creates the new permissions" do file_id = test_folder.id expect(current_remote_permissions).to eq([]) - input_data = Storages::Peripherals::StorageInteraction::Inputs::SetPermissions - .build(file_id:, user_permissions:) - .value! + input_data = Storages::Adapters::Input::SetPermissions.build(file_id:, user_permissions:).value! result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success @@ -86,44 +77,41 @@ RSpec.shared_examples_for "set_permissions_command: creates new permissions" do end end -RSpec.shared_examples_for "set_permissions_command: unknown remote identity" do +RSpec.shared_examples_for "adapter set_permissions_command: unknown remote identity" do it "returns a failure" do file_id = test_folder.id - input_data = Storages::Peripherals::StorageInteraction::Inputs::SetPermissions - .build(file_id:, user_permissions:) - .value! + input_data = Storages::Adapters::Input::SetPermissions.build(file_id:, user_permissions:).value! result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:unknown_remote_identity) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(described_class) ensure clean_up file_id end end -RSpec.shared_examples_for "set_permissions_command: not found" do +RSpec.shared_examples_for "adapter set_permissions_command: not found" do it "returns a failure" do result = described_class.call(storage:, auth_strategy:, input_data:) - expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:not_found) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(error_source) end end -RSpec.shared_examples_for "set_permissions_command: error" do +RSpec.shared_examples_for "adapter set_permissions_command: error" do it "returns a failure" do result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:error) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(described_class) end end diff --git a/modules/storages/spec/support/shared_examples_for_adapters/upload_link_query_examples.rb b/modules/storages/spec/support/shared_examples_for_storage_providers/upload_link_query_examples.rb similarity index 69% rename from modules/storages/spec/support/shared_examples_for_adapters/upload_link_query_examples.rb rename to modules/storages/spec/support/shared_examples_for_storage_providers/upload_link_query_examples.rb index 5b890666812..5cb5ee615ab 100644 --- a/modules/storages/spec/support/shared_examples_for_adapters/upload_link_query_examples.rb +++ b/modules/storages/spec/support/shared_examples_for_storage_providers/upload_link_query_examples.rb @@ -28,9 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -RSpec.shared_examples_for "upload_link_query: basic query setup" do +RSpec.shared_examples_for "adapter upload_link_query: basic query setup" do it "is registered as queries.upload_link" do - expect(Storages::Peripherals::Registry + expect(Storages::Adapters::Registry .resolve("#{storage}.queries.upload_link")).to eq(described_class) end @@ -38,46 +38,44 @@ RSpec.shared_examples_for "upload_link_query: basic query setup" do expect(described_class).to respond_to(:call) method = described_class.method(:call) - expect(method.parameters).to contain_exactly(%i[keyreq storage], - %i[keyreq auth_strategy], - %i[keyreq upload_data]) + expect(method.parameters).to contain_exactly(%i[keyreq storage], %i[keyreq auth_strategy], %i[keyreq input_data]) end end -RSpec.shared_examples_for "upload_link_query: successful upload link response" do +RSpec.shared_examples_for "adapter upload_link_query: successful upload link response" do it "returns an upload link" do - result = described_class.call(storage:, auth_strategy:, upload_data:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_success - response = result.result - expect(response).to be_a(Storages::UploadLink) + response = result.value! + expect(response).to be_a(Storages::Adapters::Results::UploadLink) expect(response.destination).to be_a(URI) expect(response.destination.to_s).to eq(upload_url) expect(response.method).to eq(upload_method) end end -RSpec.shared_examples_for "upload_link_query: not found" do +RSpec.shared_examples_for "adapter upload_link_query: not found" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, upload_data:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:not_found) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(described_class) end end -RSpec.shared_examples_for "upload_link_query: error" do +RSpec.shared_examples_for "adapter upload_link_query: error" do it "returns a failure" do - result = described_class.call(storage:, auth_strategy:, upload_data:) + result = described_class.call(storage:, auth_strategy:, input_data:) expect(result).to be_failure - error = result.errors + error = result.failure expect(error.code).to eq(:error) - expect(error.data.source).to eq(error_source) + expect(error.source).to eq(described_class) end end diff --git a/modules/storages/spec/workers/storages/copy_project_folders_job_spec.rb b/modules/storages/spec/workers/storages/copy_project_folders_job_spec.rb index 231e68702c5..6c869654578 100644 --- a/modules/storages/spec/workers/storages/copy_project_folders_job_spec.rb +++ b/modules/storages/spec/workers/storages/copy_project_folders_job_spec.rb @@ -44,19 +44,15 @@ RSpec.describe Storages::CopyProjectFoldersJob, :job, :webmock, with_good_job: S let(:target_work_packages) { create_list(:work_package, 4, project: target.project) } let(:work_packages_map) do - source_work_packages - .pluck(:id) - .map(&:to_s) - .zip(target_work_packages.pluck(:id)) - .to_h + source_work_packages.pluck(:id).map(&:to_s).zip(target_work_packages.pluck(:id)).to_h end let(:polling_url) { "https://polling.url.de/cool/subresources" } - let(:copy_result) { Storages::Peripherals::StorageInteraction::ResultData::CopyTemplateFolder.new(nil, nil, false) } + let(:copy_result) { Storages::Adapters::Results::CopyTemplateFolder.new(nil, nil, false) } let(:target_deep_file_ids) do source_file_links.each_with_object({}) do |fl, hash| - hash["#{target.managed_project_folder_path}#{fl.name}"] = Storages::StorageFileInfo.from_id("RANDOM_ID_#{fl.hash}") + hash["#{target.managed_project_folder_path}#{fl.name}"] = Storages::StorageFileId.new("RANDOM_ID_#{fl.hash}") end end @@ -120,22 +116,21 @@ RSpec.describe Storages::CopyProjectFoldersJob, :job, :webmock, with_good_job: S end # rubocop:disable Lint/UnusedBlockArgument + # TODO: Update to use verifying doubles for the queries and commands - 2025-02-10 @mereghost describe "managed project folders" do before do - Storages::Peripherals::Registry - .stub("#{storage}.queries.file_path_to_id_map", lambda { |storage:, auth_strategy:, folder:| - ServiceResult.success(result: target_deep_file_ids) + Storages::Adapters::Registry.stub("nextcloud.queries.file_path_to_id_map", + ->(storage:, auth_strategy:, input_data:) { Success(target_deep_file_ids) }) + + Storages::Adapters::Registry + .stub("#{storage}.queries.files_info", lambda { |storage:, auth_strategy:, input_data:| + Success(source_file_infos) }) - Storages::Peripherals::Registry - .stub("#{storage}.queries.files_info", lambda { |storage:, auth_strategy:, file_ids:| - ServiceResult.success(result: source_file_infos) - }) - - Storages::Peripherals::Registry + Storages::Adapters::Registry .stub("#{storage}.commands.copy_template_folder", - lambda { |auth_strategy:, storage:, source_path:, destination_path:| - ServiceResult.success(result: copy_result.with(id: "copied-folder", polling_url:)) + lambda { |auth_strategy:, storage:, input_data:| + Success(copy_result.with(id: "copied-folder", polling_url:)) }) end @@ -171,20 +166,20 @@ RSpec.describe Storages::CopyProjectFoldersJob, :job, :webmock, with_good_job: S end before do - Storages::Peripherals::Registry + Storages::Adapters::Registry .stub("#{storage}.commands.copy_template_folder", - lambda { |auth_strategy:, storage:, source_path:, destination_path:| - ServiceResult.success(result: copy_result.with(polling_url:, requires_polling: true)) + lambda { |auth_strategy:, storage:, input_data:| + Success(copy_result.with(polling_url:, requires_polling: true)) }) - Storages::Peripherals::Registry - .stub("#{storage}.queries.file_path_to_id_map", lambda { |storage:, auth_strategy:, folder:| - ServiceResult.success(result: target_deep_file_ids) + Storages::Adapters::Registry + .stub("#{storage}.queries.file_path_to_id_map", lambda { |storage:, auth_strategy:, input_data:| + Success(target_deep_file_ids) }) - Storages::Peripherals::Registry - .stub("#{storage}.queries.files_info", lambda { |storage:, auth_strategy:, file_ids:| - ServiceResult.success(result: source_file_infos) + Storages::Adapters::Registry + .stub("#{storage}.queries.files_info", lambda { |storage:, auth_strategy:, input_data:| + Success(source_file_infos) }) stub_request(:get, polling_url) @@ -240,13 +235,10 @@ RSpec.describe Storages::CopyProjectFoldersJob, :job, :webmock, with_good_job: S context "when an error occurs" do before do - Storages::Peripherals::Registry + Storages::Adapters::Registry .stub("#{storage}.commands.copy_template_folder", - lambda { |auth_strategy:, storage:, source_path:, destination_path:| - ServiceResult.failure( - result: :error, - errors: Storages::StorageError.new(code: :error, log_message: "General Failure reporting for duty") - ) + lambda { |auth_strategy:, storage:, input_data:| + Failure(Storages::Adapters::Results::Error.new(code: :error, source: self.class)) }) end diff --git a/spec/requests/oauth_clients/ensure_connection_flow_spec.rb b/spec/requests/oauth_clients/ensure_connection_flow_spec.rb index 2378ffe7119..bdc9b2ae0c2 100644 --- a/spec/requests/oauth_clients/ensure_connection_flow_spec.rb +++ b/spec/requests/oauth_clients/ensure_connection_flow_spec.rb @@ -37,10 +37,7 @@ RSpec.describe "/oauth_clients/:oauth_client_id/ensure_connection endpoint", :we shared_let(:oauth_client) { storage.oauth_client } before do - Storages::Peripherals::Registry.stub( - "#{storage}.queries.user", - ->(_) { ServiceResult.success } - ) + Storages::Adapters::Registry.stub("#{storage}.queries.user", ->(_) { Success() }) end describe "#ensure_connection" do @@ -65,9 +62,9 @@ RSpec.describe "/oauth_clients/:oauth_client_id/ensure_connection endpoint", :we let(:nonce) { "57a17c3f-b2ed-446e-9dd8-651ba3aec37d" } before do - Storages::Peripherals::Registry.stub( + Storages::Adapters::Registry.stub( "#{storage}.queries.user", - ->(_) { ServiceResult.failure(errors: Storages::StorageError.new(code: :unauthorized)) } + ->(_) { Failure(Storages::Adapters::Results::Error.new(code: :missing_token, source: self)) } ) allow(SecureRandom).to receive(:uuid).and_call_original.ordered diff --git a/spec/services/oauth_clients/connection_manager_one_drive_spec.rb b/spec/services/oauth_clients/connection_manager_one_drive_spec.rb index ed758d94c7c..be4ee234049 100644 --- a/spec/services/oauth_clients/connection_manager_one_drive_spec.rb +++ b/spec/services/oauth_clients/connection_manager_one_drive_spec.rb @@ -77,7 +77,7 @@ RSpec.describe OAuthClients::ConnectionManager, :oauth_connection_helpers, :webm it "returns a failure", :aggregate_failures do result = subject.code_to_token(code) expect(result).to be_failure - expect(result.errors).to be_a(Storages::StorageError) + expect(result.errors).to be_a(Storages::Adapters::Results::Error) end it "does not create a token" do diff --git a/spec/services/remote_identities/create_service_spec.rb b/spec/services/remote_identities/create_service_spec.rb index 566d1ea67ae..cc74038452f 100644 --- a/spec/services/remote_identities/create_service_spec.rb +++ b/spec/services/remote_identities/create_service_spec.rb @@ -19,7 +19,7 @@ RSpec.describe RemoteIdentities::CreateService, :storage_server_helpers, type: : before do allow(OpenProject::Notifications).to receive(:send) - allow(integration).to receive(:extract_origin_user_id).and_return(ServiceResult.success(result: "the-extracted-user-id")) + allow(integration).to receive(:extract_origin_user_id).and_return(Success("the-extracted-user-id")) end describe ".call" do diff --git a/spec/support/storage_server_helpers.rb b/spec/support/storage_server_helpers.rb index d8e19ed6ab8..70e27915b69 100644 --- a/spec/support/storage_server_helpers.rb +++ b/spec/support/storage_server_helpers.rb @@ -154,11 +154,11 @@ module StorageServerHelpers } stub_request(:propfind, normalize_url("#{storage.host}/remote.php/dav/files/#{remote_identity.origin_user_id}")) - .to_return(status: 207, body: root_xml_response, headers: {}) + .to_return(status: 207, body: root_xml_response, headers: { "Content-Type": "application/xml" }) stub_request(:propfind, normalize_url("#{storage.host}/remote.php/dav/files/#{remote_identity.origin_user_id}/Folder1")) - .to_return(status: 207, body: folder1_xml_response, headers: {}) + .to_return(status: 207, body: folder1_xml_response, headers: { "Content-Type": "application/xml" }) stub_request(:get, normalize_url("#{storage.host}/ocs/v1.php/apps/integration_openproject/fileinfo/11")) - .to_return(status: 200, body: folder1_fileinfo_response.to_json, headers: {}) + .to_return(status: 200, body: folder1_fileinfo_response.to_json, headers: { "Content-Type": "application/json" }) stub_nextcloud_user_query(storage.host) stub_request( :delete,