Skip to content

Commit

Permalink
Merge pull request #14954 from opf/implementation/53368-add-authentic…
Browse files Browse the repository at this point in the history
…ation-code-and-tests

[#53368] add authentication for storage interaction
  • Loading branch information
Kharonus authored Mar 20, 2024
2 parents 46ac2be + bf8882c commit 8996a6c
Show file tree
Hide file tree
Showing 25 changed files with 1,989 additions and 25 deletions.
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.8'

networks:
network:
testing:
Expand Down
1 change: 1 addition & 0 deletions lib/open_project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def self.httpx
Thread.current[:httpx_session] ||= begin
session = HTTPX
.plugin(:persistent) # persistent plugin enables retries plugin under the hood
.plugin(:oauth)
.plugin(:basic_auth)
.plugin(:webdav)
.with(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def scope = raise ::Storages::Errors::SubclassResponsibility

def basic_rack_oauth_client = raise ::Storages::Errors::SubclassResponsibility

def to_httpx_oauth_config = raise ::Storages::Errors::SubclassResponsibility

private

def authorization_check_wrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,26 @@ module Storages
module Peripherals
module OAuthConfigurations
class NextcloudConfiguration < ConfigurationInterface
Util = StorageInteraction::Nextcloud::Util

attr_reader :oauth_client

# rubocop:disable Lint/MissingSuper
def initialize(storage)
@uri = storage.uri
@oauth_client = storage.oauth_client.freeze
end

def authorization_state_check(token)
util = ::Storages::Peripherals::StorageInteraction::Nextcloud::Util
# rubocop:enable Lint/MissingSuper

def authorization_state_check(token)
authorization_check_wrapper do
OpenProject.httpx.get(
util.join_uri_path(@uri, '/ocs/v1.php/cloud/user'),
Util.join_uri_path(@uri, "/ocs/v1.php/cloud/user"),
headers: {
'Authorization' => "Bearer #{token}",
'OCS-APIRequest' => 'true',
'Accept' => 'application/json'
"Authorization" => "Bearer #{token}",
"OCS-APIRequest" => "true",
"Accept" => "application/json"
}
)
end
Expand All @@ -58,6 +61,15 @@ def extract_origin_user_id(rack_access_token)
rack_access_token.raw_attributes[:user_id]
end

def to_httpx_oauth_config
StorageInteraction::AuthenticationStrategies::OAuthConfiguration.new(
client_id: @oauth_client.client_id,
client_secret: @oauth_client.client_secret,
issuer: URI(Util.join_uri_path(@uri, "/index.php/apps/oauth2/api/v1")).normalize,
scope: []
)
end

def scope
[]
end
Expand All @@ -70,8 +82,8 @@ def basic_rack_oauth_client
scheme: @uri.scheme,
host: @uri.host,
port: @uri.port,
authorization_endpoint: File.join(@uri.path, "/index.php/apps/oauth2/authorize"),
token_endpoint: File.join(@uri.path, "/index.php/apps/oauth2/api/v1/token")
authorization_endpoint: Util.join_uri_path(@uri.path, "/index.php/apps/oauth2/authorize"),
token_endpoint: Util.join_uri_path(@uri.path, "/index.php/apps/oauth2/api/v1/token")
)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,47 @@ module Storages
module Peripherals
module OAuthConfigurations
class OneDriveConfiguration < ConfigurationInterface
DEFAULT_SCOPES = %w[offline_access files.readwrite.all user.read sites.readwrite.all].freeze
Util = StorageInteraction::OneDrive::Util

attr_reader :oauth_client

# rubocop:disable Lint/MissingSuper
def initialize(storage)
@storage = storage
@uri = storage.uri
@oauth_client = storage.oauth_client
@oauth_uri = URI('https://login.microsoftonline.com/').normalize
@oauth_uri = URI("https://login.microsoftonline.com/#{@storage.tenant_id}/oauth2/v2.0").normalize
end

def authorization_state_check(access_token)
util = ::Storages::Peripherals::StorageInteraction::OneDrive::Util
# rubocop:enable Lint/MissingSuper

def authorization_state_check(access_token)
authorization_check_wrapper do
OpenProject.httpx.get(
util.join_uri_path(@uri, '/v1.0/me'),
headers: { 'Authorization' => "Bearer #{access_token}", 'Accept' => 'application/json' }
Util.join_uri_path(@uri, "/v1.0/me"),
headers: { "Authorization" => "Bearer #{access_token}", "Accept" => "application/json" }
)
end
end

def extract_origin_user_id(rack_access_token)
util = ::Storages::Peripherals::StorageInteraction::OneDrive::Util

OpenProject.httpx.get(
util.join_uri_path(@uri, '/v1.0/me'),
headers: { 'Authorization' => "Bearer #{rack_access_token.access_token}", 'Accept' => 'application/json' }
).raise_for_status.json['id']
Util.join_uri_path(@uri, "/v1.0/me"),
headers: { "Authorization" => "Bearer #{rack_access_token.access_token}", "Accept" => "application/json" }
).raise_for_status.json["id"]
end

def to_httpx_oauth_config
StorageInteraction::AuthenticationStrategies::OAuthConfiguration.new(
client_id: @oauth_client.client_id,
client_secret: @oauth_client.client_secret,
issuer: @oauth_uri,
scope: %w[https://graph.microsoft.com/.default]
)
end

def scope
DEFAULT_SCOPES
%w[https://graph.microsoft.com/.default]
end

def basic_rack_oauth_client
Expand All @@ -75,8 +83,8 @@ def basic_rack_oauth_client
scheme: @oauth_uri.scheme,
host: @oauth_uri.host,
port: @oauth_uri.port,
authorization_endpoint: "/#{@storage.tenant_id}/oauth2/v2.0/authorize",
token_endpoint: "/#{@storage.tenant_id}/oauth2/v2.0/token"
authorization_endpoint: "#{@oauth_uri.path}/authorize",
token_endpoint: "#{@oauth_uri.path}/token"
)
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal:true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Storages
module Peripherals
module StorageInteraction
class Authentication
using ::Storages::Peripherals::ServiceResultRefinements

def self.[](strategy)
case strategy.key
when :basic_auth
AuthenticationStrategies::BasicAuth.new
when :oauth_user_token
AuthenticationStrategies::OAuthUserToken.new(strategy.user)
when :oauth_client_credentials
AuthenticationStrategies::OAuthClientCredentials.new
else
raise "Invalid authentication strategy '#{strategy}'"
end
end

# Checks for the current authorization state of a user on a specific file storage.
# Returns one of three results:
# - :connected If a valid user token is available
# - :failed_authorization If a user token is available, which is invalid and not refreshable
# - :error If an unexpected error occurred
def self.authorization_state(storage:, user:)
auth_strategy = AuthenticationStrategies::OAuthUserToken.strategy.with_user(user)

::Storages::Peripherals::Registry
.resolve("#{storage.short_provider_type}.queries.auth_check")
.call(storage:, auth_strategy:)
.match(
on_success: ->(result) { result },
on_failure: ->(error) { error.code == :unauthorized ? :failed_authorization : :error }
)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal:true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Storages
module Peripherals
module StorageInteraction
module AuthenticationStrategies
class BasicAuth
def self.strategy
Strategy.new(:basic_auth)
end

def call(storage:, http_options: {})
username = storage.username
password = storage.password

return build_failure(storage) if username.blank? || password.blank?

yield OpenProject.httpx.basic_auth(username, password).with(http_options)
end

private

def build_failure(storage)
log_message = "Cannot authenticate storage with basic auth. Password or username not configured."
data = ::Storages::StorageErrorData.new(source: self.class, payload: storage)
Failures::Builder.call(code: :error, log_message:, data:)
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal:true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Storages
module Peripherals
module StorageInteraction
module AuthenticationStrategies
module Failures
Builder = ->(code:, log_message:, data:) do
storage_error = ::Storages::StorageError.new(code:, log_message:, data:)
ServiceResult.failure(result: code, errors: storage_error)
end
end
end
end
end
end
Loading

0 comments on commit 8996a6c

Please sign in to comment.