Skip to content
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) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write 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 Wikis
module Adapters
module Input
class StrategyContract < DryApplicationContract
AUTH_METHODS = %i[bearer_token].to_set.freeze

params do
required(:key).filled(:symbol, included_in?: AUTH_METHODS)
optional(:token).maybe(:string)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,11 @@
#++

module Wikis::Adapters::Input
UserQuery = Data.define(:access_token)
Strategy = Data.define(:key, :token) do
private_class_method :new

def self.build(key:, token: nil, contract: StrategyContract.new)
contract.call(key:, token:).to_monad.fmap { new(**it.to_h) }
end
end
end
5 changes: 4 additions & 1 deletion modules/wikis/app/models/wikis/xwiki_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ def user_connected?(user)
end

def extract_origin_user_id(token)
resolve("queries.user").call(Wikis::Adapters::Input::UserQuery.new(access_token: token.access_token))
auth_strategy = Wikis::Adapters::Registry
.resolve("xwiki.authentication.user_bound")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 This could also be self.resolve("authentication.user_bound"), which is the much more likely call pattern for a generic caller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to the

    def extract_origin_user_id(token)
      auth_strategy = auth_strategy_for(token.user)
      resolve("queries.user").call(auth_strategy:)
    end

self here is not needed, and rubocop would cut it out 😄

.call(token.access_token)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 This doesn't change a lot for callers yet. The caller still has to obtain an access token from a random database table first to use this authentication strategy.

Even worse: Imagine building code for a caller that doesn't know which provider is going to be called. Such a caller also wouldn't know which underlying strategy (here: Bearer token) would be used. So they couldn't possibly know that calling the strategy with a bearer token is the correct thing to do.

Every authentication strategy should be able to work from the same interface. For example one thing that could most likely work is to pass a user into the auth strategy and let the auth strategy figure out how to get to the data that it needs based on the user and the provider (the latter would by default be passed through a provider.resolve(...) call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. The user passed to the strategy is the way to go 👍🏻

resolve("queries.user").call(auth_strategy:)
end

def authenticate_via_two_way_oauth2?
Expand Down
49 changes: 49 additions & 0 deletions modules/wikis/app/services/wikis/adapters/authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

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

module Wikis
module Adapters
class Authentication
class << self
# @param strategy [Input::Strategy]
def [](strategy)
auth = strategy.value_or { raise ArgumentError, "Invalid authentication strategy '#{it.inspect}'" }

case auth.key
when :bearer_token
AuthenticationStrategies::BearerToken.new(auth.token)
else
raise ArgumentError, "Unknown authentication scheme: #{auth.key}"
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -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 Wikis
module Adapters
module AuthenticationStrategies
class BearerToken
def initialize(token)
@token = token
end

def call(http_options: {}, **)
yield OpenProject.httpx.bearer_auth(@token).with(http_options)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ module Providers
module XWiki
module Queries
class UserQuery < BaseQuery
def call(input_data)
def call(auth_strategy:)
url = "#{provider.url.chomp('/')}/rest/"
handle_response(OpenProject.httpx.bearer_auth(input_data.access_token).get(url))
Authentication[auth_strategy].call(provider:) do |http|
handle_response(http.get(url))
end
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module Providers
module XWiki
Registry = Dry::Container::Namespace.new("xwiki") do
namespace("authentication") do
# ...
register(:user_bound, ->(token) { Input::Strategy.build(key: :bearer_token, token:) })
end

namespace("commands") do
Expand Down
52 changes: 52 additions & 0 deletions modules/wikis/spec/services/wikis/adapters/authentication_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

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

require "spec_helper"
require_module_spec_helper

RSpec.describe Wikis::Adapters::Authentication do
describe ".[]" do
subject(:strategy_object) { described_class[strategy] }

context "with a bearer_token strategy" do
let(:strategy) { Wikis::Adapters::Input::Strategy.build(key: :bearer_token, token: "t") }

it { is_expected.to be_a(Wikis::Adapters::AuthenticationStrategies::BearerToken) }
end

context "with a unknown strategy" do
let(:strategy) { Wikis::Adapters::Input::Strategy.build(key: :unknown) }

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

require "spec_helper"
require_module_spec_helper

RSpec.describe Wikis::Adapters::AuthenticationStrategies::BearerToken, :webmock do
let(:url) { "https://xwiki.local/rest/" }

subject(:strategy) { described_class.new("test-token") }

describe "#call" do
it "yields an http client configured with the bearer token" do
request_stub = stub_request(:get, url)
.with(headers: { "Authorization" => "Bearer test-token" })
.to_return(status: 200, body: "")

strategy.call { |http| http.get(url) }

expect(request_stub).to have_been_requested
end

it "forwards http_options to the http client" do
request_stub = stub_request(:get, url)
.with(headers: { "Authorization" => "Bearer test-token", "Accept" => "application/json" })
.to_return(status: 200, body: "")

strategy.call(http_options: { headers: { "Accept" => "application/json" } }) { |http| http.get(url) }

expect(request_stub).to have_been_requested
end
end
end
61 changes: 61 additions & 0 deletions modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

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

require "spec_helper"
require_module_spec_helper

RSpec.describe Wikis::Adapters::Input::Strategy do
describe ".build" do
subject(:result) { described_class.build(key:, token:) }

let(:token) { "some-token" }

context "with a valid key and token" do
let(:key) { :bearer_token }

it { is_expected.to be_success }
it { is_expected.to have_attributes(value!: have_attributes(key: :bearer_token, token: "some-token")) }
end

context "without a token" do
let(:key) { :bearer_token }
let(:token) { nil }

it { is_expected.to be_success }
it { is_expected.to have_attributes(value!: have_attributes(key: :bearer_token, token: nil)) }
end

context "with an unknown key" do
let(:key) { :unknown }

it { is_expected.to be_failure }
end
end
end
Loading
Loading