From f003085c5f0e96cdab5c098bbd10d7b096d3eed3 Mon Sep 17 00:00:00 2001 From: Yauheni Suhakou Date: Thu, 7 May 2026 16:49:34 +0200 Subject: [PATCH 1/5] Add Authentication strategy for wikis module - Add `BearerToken` auth strategy and all the code required by it - Updated `UserQuery` to use `BearerToken` auth strategy instead of just passing the token --- .../wikis/adapters/input/strategy_contract.rb | 44 +++++++++++++ .../input/{user_query.rb => strategy.rb} | 8 ++- .../wikis/app/models/wikis/xwiki_provider.rb | 5 +- .../services/wikis/adapters/authentication.rb | 49 +++++++++++++++ .../authentication_strategies/bearer_token.rb | 45 ++++++++++++++ .../providers/xwiki/queries/user_query.rb | 6 +- .../adapters/providers/xwiki/registry.rb | 2 +- .../wikis/adapters/authentication_spec.rb | 52 ++++++++++++++++ .../bearer_token_spec.rb | 60 ++++++++++++++++++ .../wikis/adapters/input/strategy_spec.rb | 61 +++++++++++++++++++ .../xwiki/queries/user_query_spec.rb | 11 ++-- 11 files changed, 333 insertions(+), 10 deletions(-) create mode 100644 modules/wikis/app/contracts/wikis/adapters/input/strategy_contract.rb rename modules/wikis/app/models/wikis/adapters/input/{user_query.rb => strategy.rb} (85%) create mode 100644 modules/wikis/app/services/wikis/adapters/authentication.rb create mode 100644 modules/wikis/app/services/wikis/adapters/authentication_strategies/bearer_token.rb create mode 100644 modules/wikis/spec/services/wikis/adapters/authentication_spec.rb create mode 100644 modules/wikis/spec/services/wikis/adapters/authentication_strategies/bearer_token_spec.rb create mode 100644 modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb diff --git a/modules/wikis/app/contracts/wikis/adapters/input/strategy_contract.rb b/modules/wikis/app/contracts/wikis/adapters/input/strategy_contract.rb new file mode 100644 index 000000000000..652ef5bef04e --- /dev/null +++ b/modules/wikis/app/contracts/wikis/adapters/input/strategy_contract.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module 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 diff --git a/modules/wikis/app/models/wikis/adapters/input/user_query.rb b/modules/wikis/app/models/wikis/adapters/input/strategy.rb similarity index 85% rename from modules/wikis/app/models/wikis/adapters/input/user_query.rb rename to modules/wikis/app/models/wikis/adapters/input/strategy.rb index d567bb919de2..9d6399a5177a 100644 --- a/modules/wikis/app/models/wikis/adapters/input/user_query.rb +++ b/modules/wikis/app/models/wikis/adapters/input/strategy.rb @@ -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 diff --git a/modules/wikis/app/models/wikis/xwiki_provider.rb b/modules/wikis/app/models/wikis/xwiki_provider.rb index bf2c980bf830..1d567d9b4bbc 100644 --- a/modules/wikis/app/models/wikis/xwiki_provider.rb +++ b/modules/wikis/app/models/wikis/xwiki_provider.rb @@ -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") + .call(token.access_token) + resolve("queries.user").call(auth_strategy:) end def authenticate_via_two_way_oauth2? diff --git a/modules/wikis/app/services/wikis/adapters/authentication.rb b/modules/wikis/app/services/wikis/adapters/authentication.rb new file mode 100644 index 000000000000..b82e93977817 --- /dev/null +++ b/modules/wikis/app/services/wikis/adapters/authentication.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module 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 diff --git a/modules/wikis/app/services/wikis/adapters/authentication_strategies/bearer_token.rb b/modules/wikis/app/services/wikis/adapters/authentication_strategies/bearer_token.rb new file mode 100644 index 000000000000..419258558afa --- /dev/null +++ b/modules/wikis/app/services/wikis/adapters/authentication_strategies/bearer_token.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module 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 diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/user_query.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/user_query.rb index 6e10dbd9722f..7309bc1e4cff 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/user_query.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/user_query.rb @@ -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 diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb index d05f417febfd..f67ca3a89e4f 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb @@ -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 diff --git a/modules/wikis/spec/services/wikis/adapters/authentication_spec.rb b/modules/wikis/spec/services/wikis/adapters/authentication_spec.rb new file mode 100644 index 000000000000..a69f1055b41e --- /dev/null +++ b/modules/wikis/spec/services/wikis/adapters/authentication_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +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 diff --git a/modules/wikis/spec/services/wikis/adapters/authentication_strategies/bearer_token_spec.rb b/modules/wikis/spec/services/wikis/adapters/authentication_strategies/bearer_token_spec.rb new file mode 100644 index 000000000000..5cee9abc9bd4 --- /dev/null +++ b/modules/wikis/spec/services/wikis/adapters/authentication_strategies/bearer_token_spec.rb @@ -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 diff --git a/modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb b/modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb new file mode 100644 index 000000000000..c8a22a336be8 --- /dev/null +++ b/modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb @@ -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 diff --git a/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/user_query_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/user_query_spec.rb index ea0289699c35..22311bb3da65 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/user_query_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/user_query_spec.rb @@ -34,7 +34,7 @@ RSpec.describe Wikis::Adapters::Providers::XWiki::Queries::UserQuery, :webmock do let(:wiki_provider) { build_stubbed(:xwiki_provider, url: "https://xwiki.local/") } let(:rest_url) { "https://xwiki.local/rest/" } - let(:input_data) { Wikis::Adapters::Input::UserQuery.new(access_token: "some-token") } + let(:auth_strategy) { Wikis::Adapters::Input::Strategy.build(key: :bearer_token, token: "some-token") } subject(:query) { described_class.new(model: wiki_provider) } @@ -46,11 +46,12 @@ context "when the request succeeds with xwiki-user header" do before do stub_request(:get, rest_url) + .with(headers: { "Authorization" => "Bearer some-token" }) .to_return(status: 200, body: "", headers: { "xwiki-user" => "XWiki.admin" }) end it "returns Success with the xwiki-user header value" do - result = query.call(input_data) + result = query.call(auth_strategy:) expect(result).to be_success expect(result.value!).to eq("XWiki.admin") end @@ -63,7 +64,7 @@ end it "returns Failure with :unauthorized code" do - result = query.call(Wikis::Adapters::Input::UserQuery.new(access_token: "invalid-token")) + result = query.call(auth_strategy: Wikis::Adapters::Input::Strategy.build(key: :bearer_token, token: "invalid-token")) expect(result).to be_failure expect(result.failure).to have_attributes(code: :unauthorized) end @@ -75,7 +76,7 @@ end it "returns Failure with :request_failed code" do - result = query.call(input_data) + result = query.call(auth_strategy:) expect(result).to be_failure expect(result.failure).to have_attributes(code: :request_failed) end @@ -85,7 +86,7 @@ before { stub_request(:get, rest_url).to_timeout } it "returns Failure with :connection_error code" do - result = query.call(input_data) + result = query.call(auth_strategy:) expect(result).to be_failure expect(result.failure).to have_attributes(code: :connection_error) end From b992f855a4e0690cf97638cbe9ccc4a0e0a05227 Mon Sep 17 00:00:00 2001 From: Yauheni Suhakou Date: Fri, 8 May 2026 17:03:03 +0200 Subject: [PATCH 2/5] Add `UserBound` auth strategy to clean up the code --- .../xwiki/authentication/user_bound.rb | 47 +++++++++++++++++ .../adapters/providers/xwiki/registry.rb | 2 +- .../xwiki/authentication/user_bound_spec.rb | 50 +++++++++++++++++++ .../xwiki/queries/user_query_spec.rb | 12 ++++- 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 modules/wikis/app/services/wikis/adapters/providers/xwiki/authentication/user_bound.rb create mode 100644 modules/wikis/spec/services/wikis/adapters/providers/xwiki/authentication/user_bound_spec.rb diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/authentication/user_bound.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/authentication/user_bound.rb new file mode 100644 index 000000000000..4e73c0e55926 --- /dev/null +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/authentication/user_bound.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Wikis + module Adapters + module Providers + module XWiki + module Authentication + class UserBound + def initialize(_model:); end + + def call(user) + Input::Strategy.build(key: :bearer_token, user:) + end + end + end + end + end + end +end diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb index f67ca3a89e4f..84572258088b 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/registry.rb @@ -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:) }) + register(:user_bound, Authentication::UserBound) end namespace("commands") do diff --git a/modules/wikis/spec/services/wikis/adapters/providers/xwiki/authentication/user_bound_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/xwiki/authentication/user_bound_spec.rb new file mode 100644 index 000000000000..3eca41a4355f --- /dev/null +++ b/modules/wikis/spec/services/wikis/adapters/providers/xwiki/authentication/user_bound_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write 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::Providers::XWiki::Authentication::UserBound do + let(:user) { build_stubbed(:user) } + + subject(:user_bound) { described_class.new(model: nil) } + + it "is registered" do + expect(Wikis::Adapters::Registry.resolve("xwiki.authentication.user_bound")).to eq(described_class) + end + + describe "#call" do + it "returns a Success with a bearer_token strategy carrying the user" do + result = user_bound.call(user) + expect(result).to be_success + expect(result.value!).to have_attributes(key: :bearer_token, user:) + end + end +end diff --git a/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/user_query_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/user_query_spec.rb index 22311bb3da65..6275e21b2b96 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/user_query_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/xwiki/queries/user_query_spec.rb @@ -32,12 +32,20 @@ require_module_spec_helper RSpec.describe Wikis::Adapters::Providers::XWiki::Queries::UserQuery, :webmock do + let(:user) { build_stubbed(:user) } + let(:oauth_client) { build_stubbed(:oauth_client) } let(:wiki_provider) { build_stubbed(:xwiki_provider, url: "https://xwiki.local/") } let(:rest_url) { "https://xwiki.local/rest/" } - let(:auth_strategy) { Wikis::Adapters::Input::Strategy.build(key: :bearer_token, token: "some-token") } + let(:auth_strategy) { Wikis::Adapters::Input::Strategy.build(key: :bearer_token, user:) } subject(:query) { described_class.new(model: wiki_provider) } + before do + allow(wiki_provider).to receive(:oauth_client).and_return(oauth_client) + allow(OAuthClientToken).to receive(:for_user_and_client).with(user, oauth_client) + .and_return(instance_double(ActiveRecord::Relation, first: instance_double(OAuthClientToken, access_token: "some-token"))) + end + it "is registered" do expect(Wikis::Adapters::Registry.resolve("xwiki.queries.user")).to eq(described_class) end @@ -64,7 +72,7 @@ end it "returns Failure with :unauthorized code" do - result = query.call(auth_strategy: Wikis::Adapters::Input::Strategy.build(key: :bearer_token, token: "invalid-token")) + result = query.call(auth_strategy:) expect(result).to be_failure expect(result.failure).to have_attributes(code: :unauthorized) end From 73b675a6c9bd82a295e211e9509be25b45ea87ee Mon Sep 17 00:00:00 2001 From: Yauheni Suhakou Date: Fri, 8 May 2026 17:25:45 +0200 Subject: [PATCH 3/5] Updated the rest of the code to use new auth --- app/services/remote_identities/create_service.rb | 2 +- .../wikis/adapters/input/strategy_contract.rb | 4 ++-- .../wikis/app/models/wikis/adapters/input/strategy.rb | 6 +++--- modules/wikis/app/models/wikis/provider.rb | 4 ++++ modules/wikis/app/models/wikis/xwiki_provider.rb | 6 ++---- .../authentication_strategies/bearer_token.rb | 9 +++++---- .../adapters/providers/xwiki/queries/page_info.rb | 2 +- .../providers/xwiki/queries/referencing_pages.rb | 2 +- .../providers/xwiki/queries/relation_page_links.rb | 2 +- modules/wikis/app/services/wikis/page_link_service.rb | 11 ++++++++--- .../wikis/text_formatting/wiki_link_matcher.rb | 4 +++- 11 files changed, 31 insertions(+), 21 deletions(-) diff --git a/app/services/remote_identities/create_service.rb b/app/services/remote_identities/create_service.rb index 34626f03e3e3..513924d4781b 100644 --- a/app/services/remote_identities/create_service.rb +++ b/app/services/remote_identities/create_service.rb @@ -47,7 +47,7 @@ def initialize(user:, integration:, token:, force_update: false) def call if @model.new_record? || @force_update - origin_result = @integration.extract_origin_user_id(@token) + origin_result = @integration.extract_origin_user_id(@token.user) user_id = origin_result.value_or { return ServiceResult.failure(errors: it) } diff --git a/modules/wikis/app/contracts/wikis/adapters/input/strategy_contract.rb b/modules/wikis/app/contracts/wikis/adapters/input/strategy_contract.rb index 652ef5bef04e..03e88117bb18 100644 --- a/modules/wikis/app/contracts/wikis/adapters/input/strategy_contract.rb +++ b/modules/wikis/app/contracts/wikis/adapters/input/strategy_contract.rb @@ -32,11 +32,11 @@ module Wikis module Adapters module Input class StrategyContract < DryApplicationContract - AUTH_METHODS = %i[bearer_token].to_set.freeze + AUTH_METHODS = %i[bearer_token internal].to_set.freeze params do required(:key).filled(:symbol, included_in?: AUTH_METHODS) - optional(:token).maybe(:string) + optional(:user).maybe end end end diff --git a/modules/wikis/app/models/wikis/adapters/input/strategy.rb b/modules/wikis/app/models/wikis/adapters/input/strategy.rb index 9d6399a5177a..a5ac9d60e04f 100644 --- a/modules/wikis/app/models/wikis/adapters/input/strategy.rb +++ b/modules/wikis/app/models/wikis/adapters/input/strategy.rb @@ -29,11 +29,11 @@ #++ module Wikis::Adapters::Input - Strategy = Data.define(:key, :token) do + Strategy = Data.define(:key, :user) 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) } + def self.build(key:, user: nil, contract: StrategyContract.new) + contract.call(key:, user:).to_monad.fmap { new(**it.to_h) } end end end diff --git a/modules/wikis/app/models/wikis/provider.rb b/modules/wikis/app/models/wikis/provider.rb index 4d591e843304..a6d40d49581d 100644 --- a/modules/wikis/app/models/wikis/provider.rb +++ b/modules/wikis/app/models/wikis/provider.rb @@ -44,6 +44,10 @@ class Provider < ApplicationRecord def to_s = self.class.registry_prefix def user_connected?(_user) = raise SubclassResponsibilityError + def auth_strategy_for(user) + resolve("authentication.user_bound").call(user) + end + class << self def registry_prefix = raise SubclassResponsibilityError end diff --git a/modules/wikis/app/models/wikis/xwiki_provider.rb b/modules/wikis/app/models/wikis/xwiki_provider.rb index 1d567d9b4bbc..384a10e71748 100644 --- a/modules/wikis/app/models/wikis/xwiki_provider.rb +++ b/modules/wikis/app/models/wikis/xwiki_provider.rb @@ -54,10 +54,8 @@ def user_connected?(user) OAuthClientToken.for_user_and_client(user, oauth_client).exists? end - def extract_origin_user_id(token) - auth_strategy = Wikis::Adapters::Registry - .resolve("xwiki.authentication.user_bound") - .call(token.access_token) + def extract_origin_user_id(user) + auth_strategy = auth_strategy_for(user) resolve("queries.user").call(auth_strategy:) end diff --git a/modules/wikis/app/services/wikis/adapters/authentication_strategies/bearer_token.rb b/modules/wikis/app/services/wikis/adapters/authentication_strategies/bearer_token.rb index 419258558afa..9a456fe6e6ce 100644 --- a/modules/wikis/app/services/wikis/adapters/authentication_strategies/bearer_token.rb +++ b/modules/wikis/app/services/wikis/adapters/authentication_strategies/bearer_token.rb @@ -32,12 +32,13 @@ module Wikis module Adapters module AuthenticationStrategies class BearerToken - def initialize(token) - @token = token + def initialize(user) + @user = user end - def call(http_options: {}, **) - yield OpenProject.httpx.bearer_auth(@token).with(http_options) + def call(provider:, http_options: {}, **) + token = OAuthClientToken.for_user_and_client(@user, provider.oauth_client).first + yield OpenProject.httpx.bearer_auth(token&.access_token).with(http_options) end end end diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/page_info.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/page_info.rb index 4fa7e995e742..4d6df109037d 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/page_info.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/page_info.rb @@ -34,7 +34,7 @@ module Providers module XWiki module Queries class PageInfo < BaseQuery - def call(input_data) + def call(input_data:, _auth_strategy:) titles = [ "What makes XWiki special?", "API documentation", diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/referencing_pages.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/referencing_pages.rb index 97772c13e08b..1220269b05d4 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/referencing_pages.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/referencing_pages.rb @@ -34,7 +34,7 @@ module Providers module XWiki module Queries class ReferencingPages < BaseQuery - def call(input_data) + def call(input_data:, _auth_strategy:) # TODO: use real API endpoints once available title = [ diff --git a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/relation_page_links.rb b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/relation_page_links.rb index 524270c712b7..4421cac4e454 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/relation_page_links.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/xwiki/queries/relation_page_links.rb @@ -34,7 +34,7 @@ module Providers module XWiki module Queries class RelationPageLinks < BaseQuery - def call(input_data) # rubocop:disable Metrics/AbcSize + def call(input_data:, _auth_strategy:) # rubocop:disable Metrics/AbcSize # TODO: use real API endpoints once available title = [ diff --git a/modules/wikis/app/services/wikis/page_link_service.rb b/modules/wikis/app/services/wikis/page_link_service.rb index 3445b22c038b..4b103b765e4e 100644 --- a/modules/wikis/app/services/wikis/page_link_service.rb +++ b/modules/wikis/app/services/wikis/page_link_service.rb @@ -41,9 +41,10 @@ def count(linkable) end def relation_page_link_infos_for(provider:, linkable:) + auth_strategy = provider.auth_strategy_for(User.current) Adapters::Input::RelationPageLinks.build(linkable:).bind do |input| provider.resolve("queries.relation_page_links") - .call(input) + .call(input_data: input, auth_strategy:) .either( ->(page_link_infos) { page_link_infos }, -> { [] } @@ -62,8 +63,9 @@ def referencing_wiki_page_infos_for(linkable:) Adapters::Input::ReferencingPages.build(linkable:).bind do |input| Provider.enabled.each do |provider| + auth_strategy = provider.auth_strategy_for(User.current) provider.resolve("queries.referencing_pages") - .call(input) + .call(input_data: input, auth_strategy:) # Only return page infos for successful results .fmap { referenced_in.concat(it) } end @@ -75,7 +77,10 @@ def referencing_wiki_page_infos_for(linkable:) private def page_info(provider:, identifier:) - Adapters::Input::PageInfo.build(identifier:).bind { provider.resolve("queries.page_info").call(it) } + auth_strategy = provider.auth_strategy_for(User.current) + Adapters::Input::PageInfo.build(identifier:).bind do |input| + provider.resolve("queries.page_info").call(input_data: input, auth_strategy:) + end end def page_title_service diff --git a/modules/wikis/app/services/wikis/text_formatting/wiki_link_matcher.rb b/modules/wikis/app/services/wikis/text_formatting/wiki_link_matcher.rb index 3d73ffd08572..2849efd9433d 100644 --- a/modules/wikis/app/services/wikis/text_formatting/wiki_link_matcher.rb +++ b/modules/wikis/app/services/wikis/text_formatting/wiki_link_matcher.rb @@ -77,8 +77,10 @@ def process def resolve_page(provider, identifier) return Failure() if provider.nil? + auth_strategy = provider.auth_strategy_for(User.current) + Wikis::Adapters::Input::PageInfo.build(identifier:).bind do |input| - provider.resolve("queries.page_info").call(input) + provider.resolve("queries.page_info").call(input_data: input, auth_strategy:) end end From 386c5e4a8b54941fb56bef79f0958094c49b06ab Mon Sep 17 00:00:00 2001 From: Yauheni Suhakou Date: Fri, 8 May 2026 17:27:23 +0200 Subject: [PATCH 4/5] Added `InternalUser` and `UserBound` auth strategies For internal queries --- .../services/wikis/adapters/authentication.rb | 4 +- .../internal_user.rb | 45 ++++++++++++++++++ .../internal/authentication/user_bound.rb | 47 +++++++++++++++++++ .../providers/internal/queries/page_info.rb | 31 ++++++------ .../internal/queries/referencing_pages.rb | 10 ++-- .../internal/queries/relation_page_links.rb | 10 ++-- .../adapters/providers/internal/registry.rb | 2 +- 7 files changed, 124 insertions(+), 25 deletions(-) create mode 100644 modules/wikis/app/services/wikis/adapters/authentication_strategies/internal_user.rb create mode 100644 modules/wikis/app/services/wikis/adapters/providers/internal/authentication/user_bound.rb diff --git a/modules/wikis/app/services/wikis/adapters/authentication.rb b/modules/wikis/app/services/wikis/adapters/authentication.rb index b82e93977817..3ca6e4fb7232 100644 --- a/modules/wikis/app/services/wikis/adapters/authentication.rb +++ b/modules/wikis/app/services/wikis/adapters/authentication.rb @@ -38,7 +38,9 @@ def [](strategy) case auth.key when :bearer_token - AuthenticationStrategies::BearerToken.new(auth.token) + AuthenticationStrategies::BearerToken.new(auth.user) + when :internal + AuthenticationStrategies::InternalUser.new(auth.user) else raise ArgumentError, "Unknown authentication scheme: #{auth.key}" end diff --git a/modules/wikis/app/services/wikis/adapters/authentication_strategies/internal_user.rb b/modules/wikis/app/services/wikis/adapters/authentication_strategies/internal_user.rb new file mode 100644 index 000000000000..b7e9d04e4701 --- /dev/null +++ b/modules/wikis/app/services/wikis/adapters/authentication_strategies/internal_user.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Wikis + module Adapters + module AuthenticationStrategies + class InternalUser + def initialize(user) + @user = user + end + + def call(**) + yield @user + end + end + end + end +end diff --git a/modules/wikis/app/services/wikis/adapters/providers/internal/authentication/user_bound.rb b/modules/wikis/app/services/wikis/adapters/providers/internal/authentication/user_bound.rb new file mode 100644 index 000000000000..e080084deeed --- /dev/null +++ b/modules/wikis/app/services/wikis/adapters/providers/internal/authentication/user_bound.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Wikis + module Adapters + module Providers + module Internal + module Authentication + class UserBound + def initialize(_model:); end + + def call(user) + Input::Strategy.build(key: :internal, user:) + end + end + end + end + end + end +end diff --git a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/page_info.rb b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/page_info.rb index 6356e00a5937..4336cb5d1f03 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/page_info.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/page_info.rb @@ -34,23 +34,24 @@ module Providers module Internal module Queries class PageInfo < BaseQuery - def call(input_data) - # TODO: should we accept implicit User.current or do we want to pass in a user explicitly? - wiki_page = WikiPage.visible.find_by(id: input_data.identifier) - return failure(code: :not_found) if wiki_page.nil? + def call(input_data:, auth_strategy:) + Authentication[auth_strategy].call do |user| + wiki_page = WikiPage.visible(user).find_by(id: input_data.identifier) + return failure(code: :not_found) if wiki_page.nil? - success( - Results::PageInfo.new( - identifier: input_data.identifier, - provider:, - title: wiki_page.title, - href: url_for(only_path: true, - controller: "/wiki", - action: "show", - project_id: wiki_page.project.identifier, - id: wiki_page.slug) + success( + Results::PageInfo.new( + identifier: input_data.identifier, + provider:, + title: wiki_page.title, + href: url_for(only_path: true, + controller: "/wiki", + action: "show", + project_id: wiki_page.project.identifier, + id: wiki_page.slug) + ) ) - ) + end end private diff --git a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/referencing_pages.rb b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/referencing_pages.rb index ac1cff8e3944..f6f1f9862765 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/referencing_pages.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/referencing_pages.rb @@ -34,20 +34,22 @@ module Providers module Internal module Queries class ReferencingPages < BaseQuery - def call(input_data) + def call(input_data:, auth_strategy:) success( provider.page_links .merge(ReverseInlinePageLink.all) .where(linkable: input_data.linkable) .order(created_at: :desc) - .map { page_info(provider:, identifier: it.identifier) } + .map { page_info(identifier: it.identifier, auth_strategy:) } ) end private - def page_info(provider:, identifier:) - Adapters::Input::PageInfo.build(identifier:).bind { provider.resolve("queries.page_info").call(it) } + def page_info(identifier:, auth_strategy:) + Adapters::Input::PageInfo.build(identifier:).bind do |input| + provider.resolve("queries.page_info").call(input_data: input, auth_strategy:) + end end end end diff --git a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/relation_page_links.rb b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/relation_page_links.rb index db0e98cc9157..dfb93fc0296b 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/internal/queries/relation_page_links.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/internal/queries/relation_page_links.rb @@ -34,19 +34,21 @@ module Providers module Internal module Queries class RelationPageLinks < BaseQuery - def call(input_data) + def call(input_data:, auth_strategy:) page_link_infos = provider.page_links .merge(RelationPageLink.all) .where(linkable: input_data.linkable) - .map { |page_link| page_info(page_link.identifier) } + .map { |page_link| page_info(page_link.identifier, auth_strategy:) } success(page_link_infos) end private - def page_info(identifier) - Input::PageInfo.build(identifier:).bind { provider.resolve("queries.page_info").call(it) } + def page_info(identifier, auth_strategy:) + Input::PageInfo.build(identifier:).bind do |input| + provider.resolve("queries.page_info").call(input_data: input, auth_strategy:) + end end end end diff --git a/modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb b/modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb index ce4c026d3def..e6c5411b23da 100644 --- a/modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb +++ b/modules/wikis/app/services/wikis/adapters/providers/internal/registry.rb @@ -34,7 +34,7 @@ module Providers module Internal Registry = Dry::Container::Namespace.new("internal") do namespace("authentication") do - # ... + register(:user_bound, Authentication::UserBound) end namespace("commands") do From d3ceefa82c253d4a260c47a551e44465b8fea057 Mon Sep 17 00:00:00 2001 From: Yauheni Suhakou Date: Fri, 8 May 2026 17:31:27 +0200 Subject: [PATCH 5/5] Updated the specs --- .../wikis/adapters/authentication_spec.rb | 10 +++- .../bearer_token_spec.rb | 15 ++++-- .../internal_user_spec.rb | 46 +++++++++++++++++ .../wikis/adapters/input/strategy_spec.rb | 21 +++----- .../authentication/user_bound_spec.rb | 51 +++++++++++++++++++ .../internal/queries/page_info_query_spec.rb | 3 +- .../queries/referencing_pages_query_spec.rb | 3 +- .../queries/relation_page_links_query_spec.rb | 3 +- 8 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 modules/wikis/spec/services/wikis/adapters/authentication_strategies/internal_user_spec.rb create mode 100644 modules/wikis/spec/services/wikis/adapters/providers/internal/authentication/user_bound_spec.rb diff --git a/modules/wikis/spec/services/wikis/adapters/authentication_spec.rb b/modules/wikis/spec/services/wikis/adapters/authentication_spec.rb index a69f1055b41e..bcd17f5f79b4 100644 --- a/modules/wikis/spec/services/wikis/adapters/authentication_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/authentication_spec.rb @@ -36,12 +36,18 @@ 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") } + let(:strategy) { Wikis::Adapters::Input::Strategy.build(key: :bearer_token, user: build_stubbed(:user)) } it { is_expected.to be_a(Wikis::Adapters::AuthenticationStrategies::BearerToken) } end - context "with a unknown strategy" do + context "with an internal strategy" do + let(:strategy) { Wikis::Adapters::Input::Strategy.build(key: :internal, user: build_stubbed(:user)) } + + it { is_expected.to be_a(Wikis::Adapters::AuthenticationStrategies::InternalUser) } + end + + context "with an unknown strategy (Failure monad)" do let(:strategy) { Wikis::Adapters::Input::Strategy.build(key: :unknown) } it "raises ArgumentError" do diff --git a/modules/wikis/spec/services/wikis/adapters/authentication_strategies/bearer_token_spec.rb b/modules/wikis/spec/services/wikis/adapters/authentication_strategies/bearer_token_spec.rb index 5cee9abc9bd4..da4e87a01f2f 100644 --- a/modules/wikis/spec/services/wikis/adapters/authentication_strategies/bearer_token_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/authentication_strategies/bearer_token_spec.rb @@ -33,8 +33,17 @@ RSpec.describe Wikis::Adapters::AuthenticationStrategies::BearerToken, :webmock do let(:url) { "https://xwiki.local/rest/" } + let(:user) { build_stubbed(:user) } + let(:oauth_client) { build_stubbed(:oauth_client) } + let(:provider) { instance_double(Wikis::XWikiProvider, oauth_client:) } + let(:oauth_client_token) { instance_double(OAuthClientToken, access_token: "test-token") } - subject(:strategy) { described_class.new("test-token") } + subject(:strategy) { described_class.new(user) } + + before do + allow(OAuthClientToken).to receive(:for_user_and_client).with(user, oauth_client) + .and_return(instance_double(ActiveRecord::Relation, first: oauth_client_token)) + end describe "#call" do it "yields an http client configured with the bearer token" do @@ -42,7 +51,7 @@ .with(headers: { "Authorization" => "Bearer test-token" }) .to_return(status: 200, body: "") - strategy.call { |http| http.get(url) } + strategy.call(provider:) { |http| http.get(url) } expect(request_stub).to have_been_requested end @@ -52,7 +61,7 @@ .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) } + strategy.call(provider:, http_options: { headers: { "Accept" => "application/json" } }) { |http| http.get(url) } expect(request_stub).to have_been_requested end diff --git a/modules/wikis/spec/services/wikis/adapters/authentication_strategies/internal_user_spec.rb b/modules/wikis/spec/services/wikis/adapters/authentication_strategies/internal_user_spec.rb new file mode 100644 index 000000000000..6f7367c1493d --- /dev/null +++ b/modules/wikis/spec/services/wikis/adapters/authentication_strategies/internal_user_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Wikis::Adapters::AuthenticationStrategies::InternalUser do + let(:user) { build_stubbed(:user) } + + subject(:strategy) { described_class.new(user) } + + describe "#call" do + it "yields the user" do + yielded = nil + strategy.call { |u| yielded = u } + expect(yielded).to eq(user) + end + end +end diff --git a/modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb b/modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb index c8a22a336be8..34e8f1709f43 100644 --- a/modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/input/strategy_spec.rb @@ -33,27 +33,22 @@ RSpec.describe Wikis::Adapters::Input::Strategy do describe ".build" do - subject(:result) { described_class.build(key:, token:) } + let(:user) { build_stubbed(:user) } - let(:token) { "some-token" } + context "with a :bearer_token key and a user" do + subject(:result) { described_class.build(key: :bearer_token, user:) } - 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")) } + it { is_expected.to be_success.and have_attributes(value!: have_attributes(key: :bearer_token, user:)) } end - context "without a token" do - let(:key) { :bearer_token } - let(:token) { nil } + context "with an :internal key and a user" do + subject(:result) { described_class.build(key: :internal, user:) } - it { is_expected.to be_success } - it { is_expected.to have_attributes(value!: have_attributes(key: :bearer_token, token: nil)) } + it { is_expected.to be_success.and have_attributes(value!: have_attributes(key: :internal, user:)) } end context "with an unknown key" do - let(:key) { :unknown } + subject(:result) { described_class.build(key: :unknown) } it { is_expected.to be_failure } end diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/authentication/user_bound_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/authentication/user_bound_spec.rb new file mode 100644 index 000000000000..d91d12d96297 --- /dev/null +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/authentication/user_bound_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write 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::Providers::Internal::Authentication::UserBound do + let(:provider) { build_stubbed(:internal_wiki_provider) } + let(:user) { build_stubbed(:user) } + + subject(:user_bound) { described_class.new(model: provider) } + + it "is registered" do + expect(Wikis::Adapters::Registry.resolve("internal.authentication.user_bound")).to eq(described_class) + end + + describe "#call" do + it "returns a Success with an internal strategy carrying the user" do + result = user_bound.call(user) + expect(result).to be_success + expect(result.value!).to have_attributes(key: :internal, user:) + end + end +end diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/page_info_query_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/page_info_query_spec.rb index 0775a571a856..393446278595 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/page_info_query_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/page_info_query_spec.rb @@ -31,10 +31,11 @@ require "spec_helper" RSpec.describe Wikis::Adapters::Providers::Internal::Queries::PageInfo do - subject { described_class.new(model: provider).call(input_data) } + subject { described_class.new(model: provider).call(input_data:, auth_strategy:) } let(:provider) { create(:internal_wiki_provider) } let(:input_data) { Wikis::Adapters::Input::PageInfo.build(identifier:).value! } + let(:auth_strategy) { provider.auth_strategy_for(current_user) } let(:identifier) { wiki_page.id.to_s } let(:wiki_page) { create(:wiki_page) } diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/referencing_pages_query_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/referencing_pages_query_spec.rb index b49ed483d0c9..2b1936b359d5 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/referencing_pages_query_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/referencing_pages_query_spec.rb @@ -31,10 +31,11 @@ require "spec_helper" RSpec.describe Wikis::Adapters::Providers::Internal::Queries::ReferencingPages do - subject { described_class.new(model: provider).call(input_data) } + subject { described_class.new(model: provider).call(input_data:, auth_strategy:) } let(:provider) { create(:internal_wiki_provider) } let(:input_data) { Wikis::Adapters::Input::ReferencingPages.build(linkable:).value! } + let(:auth_strategy) { provider.auth_strategy_for(current_user) } let(:linkable) { create(:work_package) } let(:wiki_page) { create(:wiki_page) } diff --git a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/relation_page_links_query_spec.rb b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/relation_page_links_query_spec.rb index fbf168eb3620..22042dac162f 100644 --- a/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/relation_page_links_query_spec.rb +++ b/modules/wikis/spec/services/wikis/adapters/providers/internal/queries/relation_page_links_query_spec.rb @@ -31,10 +31,11 @@ require "spec_helper" RSpec.describe Wikis::Adapters::Providers::Internal::Queries::RelationPageLinks do - subject { described_class.new(model: provider).call(input_data) } + subject { described_class.new(model: provider).call(input_data:, auth_strategy:) } let(:provider) { create(:internal_wiki_provider) } let(:input_data) { Wikis::Adapters::Input::RelationPageLinks.build(linkable: work_package).value! } + let(:auth_strategy) { provider.auth_strategy_for(current_user) } let(:wiki_page) { create(:wiki_page) } let(:project) { wiki_page.project }