+
+ <%= f.check_box :enabled, container_class: '-middle' %>
+
+ <%= t('oauth.application.instructions.enabled') %>
+
+
+
<%= f.text_field :name, required: true, size: 25, container_class: '-middle' %>
diff --git a/app/views/oauth/applications/index.html.erb b/app/views/oauth/applications/index.html.erb
index 3f290aa417bf..c9dc6f66d2ae 100644
--- a/app/views/oauth/applications/index.html.erb
+++ b/app/views/oauth/applications/index.html.erb
@@ -24,8 +24,8 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
-
++#%>
+
<% html_title t(:label_administration), t("oauth.application.plural") -%>
<%=
@@ -50,4 +50,4 @@ See COPYRIGHT and LICENSE files for more details.
end
%>
-<%= render ::OAuth::Applications::TableComponent.new(rows: @applications) %>
+<%= render ::OAuth::Applications::IndexComponent.new(oauth_applications: @applications) %>
diff --git a/app/views/oauth/applications/show.html.erb b/app/views/oauth/applications/show.html.erb
index 589274cc0a28..c2f07ed2595d 100644
--- a/app/views/oauth/applications/show.html.erb
+++ b/app/views/oauth/applications/show.html.erb
@@ -29,7 +29,6 @@ See COPYRIGHT and LICENSE files for more details.
<% html_title t(:label_administration), t('oauth.application.singular'), h(@application.name) -%>
<% local_assigns[:additional_breadcrumb] = h(@application.name) %>
-
<%= render OAuth::ShowPageHeaderComponent.new(application: @application) %>
<%= render(AttributeGroups::AttributeGroupComponent.new) do |component| %>
@@ -72,6 +71,11 @@ See COPYRIGHT and LICENSE files for more details.
value: @application.confidential? ? t(:general_text_Yes) : t(:general_text_No)
) %>
+ <% component.with_attribute(
+ key: @application.class.human_attribute_name(:enabled),
+ value: @application.enabled? ? t(:general_text_Yes) : t(:general_text_No)
+ ) %>
+
<% component.with_attribute(
key: @application.class.human_attribute_name(:redirect_uri),
value: safe_join(@application.redirect_uri.split("\n"), tag(:br))
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 46080077b757..aba43849043a 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -8,6 +8,11 @@
logged_user.presence || redirect_to(signin_path(back_url: request.fullpath))
end
+ # Configure to prevent grants when the application is disabled
+ allow_grant_flow_for_client do |_grant_type, client|
+ client.enabled?
+ end
+
# If you are planning to use Doorkeeper in Rails 5 API-only application, then you might
# want to use API mode that will skip all the views management and change the way how
# Doorkeeper responds to a requests.
diff --git a/config/initializers/feature_decisions.rb b/config/initializers/feature_decisions.rb
index dea98a89d888..7531b7944083 100644
--- a/config/initializers/feature_decisions.rb
+++ b/config/initializers/feature_decisions.rb
@@ -50,3 +50,6 @@
OpenProject::FeatureDecisions.add :enable_custom_field_for_multiple_projects,
description: "Allow a custom field to be enabled for multiple projects at once. " \
"See work package #56909 for more details."
+
+OpenProject::FeatureDecisions.add :built_in_oauth_applications,
+ description: "Allows the display and use of built-in OAuth applications."
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 2a2d4a9e3790..70965d23ab3a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -712,6 +712,8 @@ en:
uid: "Client ID"
secret: "Client secret"
owner: "Owner"
+ builtin: "Builtin"
+ enabled: "Active"
redirect_uri: "Redirect URI"
client_credentials_user_id: "Client Credentials User ID"
scopes: "Scopes"
@@ -2148,6 +2150,7 @@ en:
label_copy_project: "Copy project"
label_core_version: "Core version"
label_core_build: "Core build"
+ label_created_by: "Created by %{user}"
label_current_status: "Current status"
label_current_version: "Current version"
label_custom_field_add_no_type: "Add this field to a work package type"
@@ -4001,12 +4004,16 @@ en:
oauth:
application:
+ builtin: Built-in instance application
+ confidential: Confidential
singular: "OAuth application"
plural: "OAuth applications"
named: "OAuth application '%{name}'"
new: "New OAuth application"
+ non_confidential: Non confidential
default_scopes: "(Default scopes)"
instructions:
+ enabled: "Enable this application, allowing users to perform authorization grants with it."
name: "The name of your application. This will be displayed to other users upon authorization."
redirect_uri_html: >
The allowed URLs authorized users can be redirected to. One entry per line.
@@ -4017,6 +4024,10 @@ en:
client_credential_user_id: "Optional user ID to impersonate when clients use this application. Leave empty to allow public access only"
register_intro: "If you are developing an OAuth API client application for OpenProject, you can register it using this form for all users to use."
default_scopes: ""
+ header:
+ builtin_applications: Built-in OAuth applications
+ other_applications: Other OAuth applications
+ empty_application_lists: No OAuth applications have been registered.
client_id: "Client ID"
client_secret_notice: >
This is the only time we can print the client secret, please note it down and keep it secure.
diff --git a/config/routes.rb b/config/routes.rb
index d11ea75f617a..e4c9dbe73083 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -465,7 +465,11 @@
resources :custom_actions, except: :show
namespace :oauth do
- resources :applications
+ resources :applications do
+ member do
+ post :toggle
+ end
+ end
end
end
diff --git a/db/migrate/20240513135928_extend_oauth_applications.rb b/db/migrate/20240513135928_extend_oauth_applications.rb
new file mode 100644
index 000000000000..18ec11c1fe37
--- /dev/null
+++ b/db/migrate/20240513135928_extend_oauth_applications.rb
@@ -0,0 +1,8 @@
+class ExtendOAuthApplications < ActiveRecord::Migration[7.1]
+ def change
+ change_table :oauth_applications, bulk: true do |t|
+ t.column :enabled, :boolean, default: true, null: false
+ t.column :builtin, :boolean, default: false, null: false
+ end
+ end
+end
diff --git a/modules/bim/spec/features/bcf/api_authorization_spec.rb b/modules/bim/spec/features/bcf/api_authorization_spec.rb
index c02100f61589..5ae2fc055fea 100644
--- a/modules/bim/spec/features/bcf/api_authorization_spec.rb
+++ b/modules/bim/spec/features/bcf/api_authorization_spec.rb
@@ -46,7 +46,7 @@ def oauth_path(client_id)
it "can create and later authorize and manage an OAuth application grant and then use the access token for the bcf api" do
# Initially empty
- expect(page).to have_css(".generic-table--empty-row", text: "There is currently nothing to display")
+ expect(page).to have_test_selector("op-admin-oauth--applications-placeholder")
# Create application
page.find_test_selector("op-admin-oauth--button-new", text: "OAuth application").click
diff --git a/modules/storages/app/services/storages/oauth_applications/create_service.rb b/modules/storages/app/services/storages/oauth_applications/create_service.rb
index 618e73630d11..76120a879a87 100644
--- a/modules/storages/app/services/storages/oauth_applications/create_service.rb
+++ b/modules/storages/app/services/storages/oauth_applications/create_service.rb
@@ -42,16 +42,16 @@ def initialize(storage:, user:)
end
def call
- ::OAuth::PersistApplicationService
- .new(::Doorkeeper::Application.new, user:)
- .call({
- name: "#{storage.name} (#{I18n.t("storages.provider_types.#{storage.short_provider_type}.name")})",
- redirect_uri: File.join(storage.host, "index.php/apps/integration_openproject/oauth-redirect"),
- scopes: "api_v3",
- confidential: true,
- owner: storage.creator,
- integration: storage
- })
+ ::OAuth::Applications::CreateService
+ .new(user:)
+ .call(
+ name: "#{storage.name} (#{I18n.t("storages.provider_types.#{storage.short_provider_type}.name")})",
+ redirect_uri: File.join(storage.host, "index.php/apps/integration_openproject/oauth-redirect"),
+ scopes: "api_v3",
+ confidential: true,
+ owner: storage.creator,
+ integration: storage
+ )
end
end
end
diff --git a/modules/storages/app/services/storages/storages/update_service.rb b/modules/storages/app/services/storages/storages/update_service.rb
index 0bb1448fcad0..efa30027ebbd 100644
--- a/modules/storages/app/services/storages/storages/update_service.rb
+++ b/modules/storages/app/services/storages/storages/update_service.rb
@@ -37,12 +37,12 @@ def after_perform(service_call)
storage = service_call.result
if storage.provider_type_nextcloud?
application = storage.oauth_application
- persist_service_result = ::OAuth::PersistApplicationService
- .new(application, user:)
- .call({
- name: "#{storage.name} (#{I18n.t("storages.provider_types.#{storage.short_provider_type}.name")})",
- redirect_uri: File.join(storage.host, "index.php/apps/integration_openproject/oauth-redirect")
- })
+ persist_service_result = ::OAuth::Applications::UpdateService
+ .new(model: application, user:)
+ .call(
+ name: "#{storage.name} (#{I18n.t("storages.provider_types.#{storage.short_provider_type}.name")})",
+ redirect_uri: File.join(storage.host, "index.php/apps/integration_openproject/oauth-redirect")
+ )
service_call.add_dependent!(persist_service_result)
end
diff --git a/spec/contracts/oauth/applications/base_contract_spec.rb b/spec/contracts/oauth/applications/base_contract_spec.rb
new file mode 100644
index 000000000000..1aa2a9111fb9
--- /dev/null
+++ b/spec/contracts/oauth/applications/base_contract_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write 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_relative "shared_examples"
+
+RSpec.describe OAuth::Applications::BaseContract, type: :model do # rubocop:disable RSpec/SpecFilePathFormat
+ let(:user) { create(:admin) }
+
+ subject { described_class.new(application, user).validate }
+
+ describe ":user" do
+ let(:application) { create(:oauth_application) }
+
+ context "if user is admin" do
+ it_behaves_like "oauth application contract is valid"
+ end
+
+ context "if user is not admin" do
+ let(:user) { create(:user) }
+
+ it_behaves_like "oauth application contract is invalid"
+ end
+ end
+
+ describe ":integration" do
+ context "if only integration id and not integration type is given" do
+ let(:application) { create(:oauth_application, integration_id: 1) }
+
+ it_behaves_like "oauth application contract is invalid"
+ end
+
+ context "if only integration type and not integration id is given" do
+ let(:application) { create(:oauth_application, integration_type: "Storages::NextcloudStorage") }
+
+ it_behaves_like "oauth application contract is invalid"
+ end
+
+ context "if both integration type and integration id is given" do
+ let(:storage) { create(:nextcloud_storage) }
+ let(:application) { create(:oauth_application, integration: storage) }
+
+ it_behaves_like "oauth application contract is valid"
+ end
+ end
+
+ describe ":client_credentials_user_id" do
+ let(:secret) { "my_secret" }
+
+ context "if no client credential user is defined" do
+ let(:application) { build_stubbed(:oauth_application, secret:) }
+
+ it_behaves_like "oauth application contract is valid"
+ end
+
+ context "if client credential user is defined and present" do
+ let(:auth_user) { create(:user) }
+ let(:application) { build_stubbed(:oauth_application, secret:, client_credentials_user_id: auth_user.id) }
+
+ it_behaves_like "oauth application contract is valid"
+ end
+
+ context "if client credential user is defined and not present" do
+ let(:application) { build_stubbed(:oauth_application, secret:, client_credentials_user_id: "1337") }
+
+ it_behaves_like "oauth application contract is invalid"
+ end
+ end
+end
diff --git a/spec/contracts/oauth/applications/create_contract_spec.rb b/spec/contracts/oauth/applications/create_contract_spec.rb
new file mode 100644
index 000000000000..c0d00a938e5e
--- /dev/null
+++ b/spec/contracts/oauth/applications/create_contract_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_relative "shared_examples"
+
+RSpec.describe OAuth::Applications::CreateContract, type: :model do # rubocop:disable RSpec/SpecFilePathFormat
+ let(:user) { create(:admin) }
+
+ subject { described_class.new(application, user).validate }
+
+ context "if no owner is given" do
+ let(:application) { create(:oauth_application, owner: nil) }
+
+ it_behaves_like "oauth application contract is invalid"
+ end
+
+ context "if owner is given" do
+ let(:application) { create(:oauth_application, owner: user) }
+
+ it_behaves_like "oauth application contract is valid"
+ end
+end
diff --git a/spec/contracts/oauth/applications/delete_contract_spec.rb b/spec/contracts/oauth/applications/delete_contract_spec.rb
new file mode 100644
index 000000000000..91e1cedff4ee
--- /dev/null
+++ b/spec/contracts/oauth/applications/delete_contract_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_relative "shared_examples"
+
+RSpec.describe OAuth::Applications::DeleteContract, type: :model do # rubocop:disable RSpec/SpecFilePathFormat
+ subject { described_class.new(application, user).validate }
+
+ context "if oauth application is builtin" do
+ let(:user) { create(:admin) }
+ let(:application) { create(:oauth_application, builtin: true) }
+
+ it_behaves_like "oauth application contract is invalid"
+ end
+
+ context "if user is no admin" do
+ let(:user) { create(:user) }
+ let(:application) { create(:oauth_application) }
+
+ it_behaves_like "oauth application contract is invalid"
+ end
+end
diff --git a/spec/contracts/oauth/applications/shared_examples.rb b/spec/contracts/oauth/applications/shared_examples.rb
new file mode 100644
index 000000000000..17094c0f0a40
--- /dev/null
+++ b/spec/contracts/oauth/applications/shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+RSpec.shared_examples_for "oauth application contract is invalid" do
+ it { expect(subject).to be_falsey }
+end
+
+RSpec.shared_examples_for "oauth application contract is valid" do
+ it { expect(subject).to be_truthy }
+end
diff --git a/spec/contracts/oauth/applications/update_contract_spec.rb b/spec/contracts/oauth/applications/update_contract_spec.rb
new file mode 100644
index 000000000000..1474dfd97b16
--- /dev/null
+++ b/spec/contracts/oauth/applications/update_contract_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+require_relative "shared_examples"
+
+RSpec.describe OAuth::Applications::UpdateContract, type: :model do # rubocop:disable RSpec/SpecFilePathFormat
+ subject { described_class.new(application, user).validate }
+
+ context "if application is builtin" do
+ let(:user) { create(:admin) }
+ let(:application) { create(:oauth_application, builtin: true) }
+
+ it_behaves_like "oauth application contract is invalid"
+ end
+end
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index 797f276150e6..682df0c2bbac 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -30,7 +30,7 @@
require "work_package"
RSpec.describe OAuth::ApplicationsController do
- let(:user) { build_stubbed(:admin) }
+ shared_let(:user) { create(:admin) }
let(:application_stub) { build_stubbed(:oauth_application, id: 1, secret: "foo") }
before do
@@ -38,7 +38,7 @@
end
context "not logged as admin" do
- let(:user) { build_stubbed(:user) }
+ shared_let(:user) { create(:user) }
it "does not grant access" do
get :index
@@ -85,18 +85,16 @@
end
describe "#create" do
- before do
- allow(Doorkeeper::Application)
- .to receive(:new)
- .and_return(application_stub)
- expect(application_stub).to receive(:attributes=)
- expect(application_stub).to receive(:save).and_return(true)
- expect(application_stub).to receive(:plaintext_secret).and_return("secret!")
- end
-
it do
- post :create, params: { application: { name: "foo" } }
- expect(response).to redirect_to action: :show, id: application_stub.id
+ post :create, params: {
+ application: {
+ name: "foo",
+ redirect_uri: "urn:ietf:wg:oauth:2.0:oob"
+ }
+ }
+ expect(response).to be_redirect
+ app = Doorkeeper::Application.last
+ expect(app.name).to eq "foo"
end
end
diff --git a/spec/features/admin/oauth/oauth_applications_management_spec.rb b/spec/features/admin/oauth/oauth_applications_management_spec.rb
index aa775cb2f798..fd7c4bbbb1c6 100644
--- a/spec/features/admin/oauth/oauth_applications_management_spec.rb
+++ b/spec/features/admin/oauth/oauth_applications_management_spec.rb
@@ -29,16 +29,17 @@
require "spec_helper"
RSpec.describe "OAuth applications management", :js, :with_cuprite do
- let(:admin) { create(:admin) }
+ shared_let(:admin) { create(:admin) }
before do
login_as admin
- visit oauth_applications_path
end
it "can create, update, show and delete applications" do
+ visit oauth_applications_path
+
# Initially empty
- expect(page).to have_css(".generic-table--empty-row", text: "There is currently nothing to display")
+ expect(page).to have_test_selector("op-admin-oauth--applications-placeholder")
# Create application
page.find_test_selector("op-admin-oauth--button-new", text: "OAuth application").click
@@ -51,7 +52,7 @@
expect(page).to have_css(".errorExplanation", text: "Redirect URI must be an absolute URI.")
fill_in("application_redirect_uri", with: "")
- # Fill rediret_uri which does not provide a Secure Context
+ # Fill redirect_uri which does not provide a Secure Context
fill_in "application_redirect_uri", with: "http://example.org"
click_on "Create"
@@ -77,7 +78,7 @@
click_on "Save"
# Show application
- find("td a", text: "My API application").click
+ click_on "My API application"
expect(page).to have_no_css(".attributes-key-value--key", text: "Client secret")
expect(page).to have_no_css(".attributes-key-value--value code")
@@ -89,6 +90,48 @@
end
# Table is empty again
- expect(page).to have_css(".generic-table--empty-row", text: "There is currently nothing to display")
+ expect(page).to have_test_selector("op-admin-oauth--applications-placeholder")
+ end
+
+ context "with a seeded application", with_flag: { built_in_oauth_applications: true } do
+ before do
+ OAuthApplicationsSeeder.new.seed_data!
+ end
+
+ it "does not allow editing or deleting the seeded application" do
+ visit oauth_applications_path
+
+ app = Doorkeeper::Application.last
+
+ within_test_selector("op-admin-oauth--built-in-applications") do
+ expect(page).to have_test_selector("op-admin-oauth--application", count: 1)
+ expect(page).to have_link(text: "OpenProject Mobile App")
+ expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "On")
+
+ find_test_selector("op-admin-oauth--application-enabled-toggle-switch").click
+ expect(page).not_to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "Loading")
+ expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "Off")
+
+ app.reload
+ expect(app).to be_builtin
+ expect(app).not_to be_enabled
+
+ find_test_selector("op-admin-oauth--application-enabled-toggle-switch").click
+ expect(page).not_to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "Loading")
+ expect(page).to have_test_selector("op-admin-oauth--application-enabled-toggle-switch", text: "On")
+
+ app.reload
+ expect(app).to be_builtin
+ expect(app).to be_enabled
+
+ click_on "OpenProject Mobile App"
+ end
+
+ expect(page).to have_no_button("Edit")
+ expect(page).to have_no_button("Delete")
+
+ visit edit_oauth_application_path(app)
+ expect(page).to have_text "You are not authorized to access this page."
+ end
end
end
diff --git a/spec/features/oauth/authorization_code_flow_spec.rb b/spec/features/oauth/authorization_code_flow_spec.rb
index caf6a3a9a1c1..5b1c95963f6c 100644
--- a/spec/features/oauth/authorization_code_flow_spec.rb
+++ b/spec/features/oauth/authorization_code_flow_spec.rb
@@ -126,6 +126,23 @@ def get_and_test_token(code)
expect(user.oauth_grants.count).to eq 0
end
+ it "does not authenticate disabled applications" do
+ app.toggle!(:enabled)
+
+ visit oauth_path app.uid, redirect_uri
+
+ # Expect we're guided to the login screen
+ login_with user.login, "adminADMIN!", visit_signin_path: false
+
+ # But we got no further
+ expect(page).to have_css(".op-toast.-error",
+ text: "The client is not authorized to perform this request using this method.")
+
+ # And also have no grant for this application
+ user.oauth_grants.reload
+ expect(user.oauth_grants.count).to eq 0
+ end
+
# Selenium can't return response headers
context "in browser that can log response headers", js: false do
before do
diff --git a/spec/services/oauth/applications/create_service_spec.rb b/spec/services/oauth/applications/create_service_spec.rb
new file mode 100644
index 000000000000..bc299a08502e
--- /dev/null
+++ b/spec/services/oauth/applications/create_service_spec.rb
@@ -0,0 +1,39 @@
+# 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.
+#++
+
+require "spec_helper"
+require "services/base_services/behaves_like_create_service"
+
+RSpec.describe OAuth::Applications::CreateService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat
+ it_behaves_like "BaseServices create service" do
+ let(:model_class) { Doorkeeper::Application }
+ let(:factory) { :oauth_application }
+ end
+end
diff --git a/spec/services/oauth/applications/delete_service_spec.rb b/spec/services/oauth/applications/delete_service_spec.rb
new file mode 100644
index 000000000000..4fcb9b55fcf0
--- /dev/null
+++ b/spec/services/oauth/applications/delete_service_spec.rb
@@ -0,0 +1,38 @@
+# 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.
+#++
+
+require "spec_helper"
+require "services/base_services/behaves_like_delete_service"
+
+RSpec.describe OAuth::Applications::DeleteService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat
+ it_behaves_like "BaseServices delete service" do
+ let(:factory) { :oauth_application }
+ end
+end
diff --git a/spec/services/oauth/applications/set_attributes_service_spec.rb b/spec/services/oauth/applications/set_attributes_service_spec.rb
new file mode 100644
index 000000000000..396522209ceb
--- /dev/null
+++ b/spec/services/oauth/applications/set_attributes_service_spec.rb
@@ -0,0 +1,100 @@
+# 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.
+#++
+
+require "spec_helper"
+
+RSpec.describe OAuth::Applications::SetAttributesService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat
+ let(:current_user) { build_stubbed(:admin) }
+
+ let(:contract_instance) do
+ contract = instance_double(OAuth::Applications::CreateContract, "contract_instance")
+ allow(contract).to receive_messages(validate: contract_valid, errors: contract_errors)
+ contract
+ end
+
+ let(:contract_errors) { instance_double(ActiveModel::Errors, "contract_errors") }
+ let(:contract_valid) { true }
+ let(:model_valid) { true }
+
+ let(:instance) do
+ described_class.new(user: current_user,
+ model: model_instance,
+ contract_class:,
+ contract_options: {})
+ end
+ let(:model_instance) { Doorkeeper::Application.new }
+ let(:contract_class) do
+ allow(OAuth::Applications::CreateContract)
+ .to receive(:new)
+ .and_return(contract_instance)
+
+ OAuth::Applications::CreateContract
+ end
+
+ let(:params) { { name: "My app" } }
+
+ before do
+ allow(model_instance)
+ .to receive(:valid?)
+ .and_return(model_valid)
+ end
+
+ subject { instance.call(params) }
+
+ it "returns the instance as the result" do
+ expect(subject.result)
+ .to eql model_instance
+ end
+
+ it "is a success" do
+ expect(subject)
+ .to be_success
+ end
+
+ context "with new record" do
+ it "sets owner to current user" do
+ expect(subject.result.owner_id).to eq current_user.id
+ expect(subject.result.owner_type).to eq "User"
+ end
+ end
+
+ context "with an invalid contract" do
+ let(:contract_valid) { false }
+
+ it "returns failure" do
+ expect(subject).not_to be_success
+ end
+
+ it "returns the contract's errors" do
+ expect(subject.errors)
+ .to eql(contract_errors)
+ end
+ end
+end
diff --git a/spec/services/oauth/applications/update_service_spec.rb b/spec/services/oauth/applications/update_service_spec.rb
new file mode 100644
index 000000000000..a8523eb94f1e
--- /dev/null
+++ b/spec/services/oauth/applications/update_service_spec.rb
@@ -0,0 +1,38 @@
+# 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.
+#++
+
+require "spec_helper"
+require "services/base_services/behaves_like_update_service"
+
+RSpec.describe OAuth::Applications::UpdateService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat
+ it_behaves_like "BaseServices update service" do
+ let(:factory) { :oauth_application }
+ end
+end