Rework of Storages Registry based approach (#17881)

* Re-creates the Registry and Errors under the Adapters namespace.
* Bring Authentication and Strategies to Adapters
* Make Strategies work with Result and clean up a bit of the code
* Setup SetPermissions Command and tests
* Moves create folder, need to add the input value
* Adds the create folder input
* RenameFile migrated
* Files Query and some Result Objects
* Gets the sync service working with the new commands/query
* UploadLinkQuery ported
* FileInfoQuery ported
* FilePathToIdMap moved
* Cleanup unused files and warnings
* Moves DeleteFolder. Updates tests of OneDriveSyncService
* Add some tests for the the inputs
* Start moving the bare minimum for the NextcloudSync
* Moves nextcloud FilePathToIdMap
* Create and Delete Folder nextcloud commands
* Port Nextcloud FileInfo and RenameFile
* Implements the changes necessary for create folder on the file picker
* Moves the CreateFolderService to the Adapters
* Move Nextcloud SetPermissions
* AuthCheck moved. Missing teests. Slowly moving the API to Adapters
* Adds note to figure out the open queries
* Move the user and group manipulation to adapters
* Moves Nextcloud FilesQuery
* Makes NextcloudSync to run on top of the new Adapter namespace
* Disable Peripherals::Registry
* Update CopyTemplateFolderService
* Makes services green again. Moves the new Nextcloud contract to Adapters
* Moves the new nextcloud contracts and fixes some the now broken tests
* Reintroduces the Internal namespace in OneDrive. Updates the contracts for Strategy to optionally take a storage (OIDC issues)
* Moves User and DownloadLink Queries and supporting code.
* Start to move the API over the new commands/queries
* Migrates the StorgeFilesAPI to the adapters
* FileLinksAPI cleared
* Updates the Storages API specs and implementations
* OpenStorage API done
* Update capabilities query
* Move connection validators and fix some broken tests
* Delete old code, update hidden dependencies.
* Adds missing handling for sso tokens
This commit is contained in:
Marcello Rocha
2025-07-10 09:01:55 +02:00
committed by GitHub
parent 5e2151abd4
commit 55ff4d6903
393 changed files with 29199 additions and 18447 deletions
-1
View File
@@ -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"
-5
View File
@@ -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
+1 -2
View File
@@ -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)
+3
View File
@@ -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
+4
View File
@@ -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.
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -29,7 +29,7 @@
#++
module Storages
module Peripherals
module Adapters
module ConnectionValidators
class BaseConnectionValidator
class << self
@@ -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) }
@@ -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
@@ -29,7 +29,7 @@
#++
module Storages
module Peripherals
module Adapters
module ConnectionValidators
class ValidationGroupResult
delegate :[], :each_pair, to: :@results
@@ -29,7 +29,7 @@
#++
module Storages
module Peripherals
module Adapters
module ConnectionValidators
class ValidatorResult
private attr_reader :group_results
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

Some files were not shown because too many files have changed in this diff Show More