diff --git a/app/components/oauth/applications/row_component.rb b/app/components/oauth/applications/row_component.rb index 70b598eac265..7820c4aa375d 100644 --- a/app/components/oauth/applications/row_component.rb +++ b/app/components/oauth/applications/row_component.rb @@ -37,8 +37,20 @@ def application model end + def builtin + checkmark(application.builtin?) + end + + def enabled + checkmark(application.enabled?) + end + def name - link_to application.name, oauth_application_path(application) + if enabled + link_to application.name, oauth_application_path(application) + else + render(Primer::Beta::Text.new(color: :muted)) { application.name } + end end def owner @@ -73,7 +85,29 @@ def edit_link end def button_links - [edit_link, helpers.delete_link(oauth_application_path(application))] + if application.builtin? + [toggle_enabled_link] + else + [edit_link, helpers.delete_link(oauth_application_path(application))] + end + end + + def toggle_enabled_link + if application.enabled? + link_to( + I18n.t(:button_deactivate), + toggle_oauth_application_path(application), + class: "oauth-application--edit-link icon icon-lock", + method: :post + ) + else + link_to( + I18n.t(:button_activate), + toggle_oauth_application_path(application), + class: "oauth-application--edit-link icon icon-unlock", + method: :post + ) + end end end end diff --git a/app/components/oauth/applications/table_component.rb b/app/components/oauth/applications/table_component.rb index 144742596aa5..6cfd96b768c5 100644 --- a/app/components/oauth/applications/table_component.rb +++ b/app/components/oauth/applications/table_component.rb @@ -66,6 +66,8 @@ def headers [ ["name", { caption: ::Doorkeeper::Application.human_attribute_name(:name) }], ["owner", { caption: ::Doorkeeper::Application.human_attribute_name(:owner) }], + ["builtin", { caption: ::Doorkeeper::Application.human_attribute_name(:builtin) }], + ["enabled", { caption: ::Doorkeeper::Application.human_attribute_name(:enabled) }], ["client_credentials", { caption: I18n.t("oauth.client_credentials") }], ["redirect_uri", { caption: ::Doorkeeper::Application.human_attribute_name(:redirect_uri) }], ["confidential", { caption: ::Doorkeeper::Application.human_attribute_name(:confidential) }] diff --git a/app/contracts/concerns/requires_admin_guard.rb b/app/contracts/concerns/requires_admin_guard.rb index b01feb96064b..8084f00ccbd8 100644 --- a/app/contracts/concerns/requires_admin_guard.rb +++ b/app/contracts/concerns/requires_admin_guard.rb @@ -35,7 +35,7 @@ module RequiresAdminGuard # Adds an error if user is archived or not an admin. def validate_admin_only - unless user.admin? && user.active? + unless user.active_admin? errors.add :base, :error_unauthorized end end diff --git a/app/contracts/delete_contract.rb b/app/contracts/delete_contract.rb index 1817df95a5a8..b2561cb9c42c 100644 --- a/app/contracts/delete_contract.rb +++ b/app/contracts/delete_contract.rb @@ -56,7 +56,7 @@ def authorized? case permission when :admin - user.admin? && user.active? + user.active_admin? when Proc instance_exec(&permission) else diff --git a/app/contracts/oauth/applications/base_contract.rb b/app/contracts/oauth/applications/base_contract.rb new file mode 100644 index 000000000000..4016cfffb51c --- /dev/null +++ b/app/contracts/oauth/applications/base_contract.rb @@ -0,0 +1,75 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OAuth + module Applications + class BaseContract < ::ModelContract + def self.model + ::Doorkeeper::Application + end + + validate :validate_client_credential_user + validate :validate_integration + validate :validate_admin_only + + attribute :enabled + attribute :name + attribute :redirect_uri + attribute :confidential + attribute :owner_id + attribute :owner_type + attribute :scopes + attribute :client_credentials_user_id + attribute :integration_id + attribute :integration_type + + private + + def validate_admin_only + unless user.active_admin? + errors.add :base, :error_unauthorized + end + end + + def validate_integration + if (model.integration_id.nil? && model.integration_type.present?) || + (model.integration_id.present? && model.integration_type.nil?) + errors.add :integration, :invalid + end + end + + def validate_client_credential_user + return if model.client_credentials_user_id.blank? + + unless User.exists?(id: model.client_credentials_user_id) + errors.add :client_credentials_user_id, :invalid + end + end + end + end +end diff --git a/app/services/oauth/persist_application_service.rb b/app/contracts/oauth/applications/create_contract.rb similarity index 57% rename from app/services/oauth/persist_application_service.rb rename to app/contracts/oauth/applications/create_contract.rb index 6f25e567c39e..402f5d694351 100644 --- a/app/services/oauth/persist_application_service.rb +++ b/app/contracts/oauth/applications/create_contract.rb @@ -27,39 +27,19 @@ #++ module OAuth - class PersistApplicationService - include Contracted + module Applications + class CreateContract < BaseContract + attribute :builtin + attribute :uid - attr_reader :application, :current_user + validate :validate_owner_present - def initialize(model, user:) - @application = model - @current_user = user + private - self.contract_class = OAuth::ApplicationContract - end - - def call(attributes) - set_defaults - application.attributes = attributes - set_secret_and_id - - result, errors = validate_and_save(application, current_user) - ServiceResult.new success: result, errors:, result: application - end - - def set_defaults - return if application.owner_id - - application.owner = current_user - application.owner_type = "User" - end - - def set_secret_and_id - application.extend(OpenProject::ChangedBySystem) - application.change_by_system do - application.renew_secret if application.secret.blank? - application.uid = Doorkeeper::OAuth::Helpers::UniqueToken.generate if application.uid.blank? + def validate_owner_present + if model.owner.blank? + errors.add(:owner, :blank) + end end end end diff --git a/app/contracts/oauth/applications/delete_contract.rb b/app/contracts/oauth/applications/delete_contract.rb new file mode 100644 index 000000000000..ba4c2368d031 --- /dev/null +++ b/app/contracts/oauth/applications/delete_contract.rb @@ -0,0 +1,35 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OAuth + module Applications + class DeleteContract < ::DeleteContract + delete_permission -> { !model.builtin? && user.active_admin? } + end + end +end diff --git a/app/contracts/oauth/applications/update_contract.rb b/app/contracts/oauth/applications/update_contract.rb new file mode 100644 index 000000000000..7c4cd202483f --- /dev/null +++ b/app/contracts/oauth/applications/update_contract.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OAuth + module Applications + class UpdateContract < BaseContract + end + end +end diff --git a/app/contracts/oauth_clients/create_contract.rb b/app/contracts/oauth_clients/create_contract.rb index ddfcc19866fb..2a76eb63c4c8 100644 --- a/app/contracts/oauth_clients/create_contract.rb +++ b/app/contracts/oauth_clients/create_contract.rb @@ -47,7 +47,7 @@ class CreateContract < ::ModelContract private def validate_user_allowed - unless user.admin? && user.active? + unless user.active_admin? errors.add :base, :error_unauthorized end end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index d77870eb8019..a5d48532f2de 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -29,8 +29,8 @@ module OAuth class ApplicationsController < ::ApplicationController before_action :require_admin - before_action :new_app, only: %i[new create] - before_action :find_app, only: %i[edit update show destroy] + before_action :find_app, only: %i[edit update show toggle destroy] + before_action :prevent_builtin_edits, only: %i[edit update destroy] layout "admin" menu_item :oauth_applications @@ -44,26 +44,34 @@ def show flash.delete :reveal_secret end - def new; end + def new + @application = ::Doorkeeper::Application.new + end def edit; end def create - call = ::OAuth::PersistApplicationService.new(@application, user: current_user) - .call(permitted_params.oauth_application) + call = ::OAuth::Applications::CreateService.new(user: current_user) + .call(permitted_params.oauth_application) if call.success? flash[:notice] = t(:notice_successful_create) flash[:_application_secret] = call.result.plaintext_secret redirect_to action: :show, id: call.result.id else + @application = call.result render action: :new end end + def toggle + @application.toggle!(:enabled) + redirect_to action: :index + end + def update - call = ::OAuth::PersistApplicationService.new(@application, user: current_user) - .call(permitted_params.oauth_application) + call = ::OAuth::Applications::UpdateService.new(model: @application, user: current_user) + .call(permitted_params.oauth_application) if call.success? flash[:notice] = t(:notice_successful_update) @@ -75,7 +83,11 @@ def update end def destroy - if @application.destroy + call = OAuth::Applications::DeleteService + .new(model: @application, user: current_user) + .call + + if call.success? flash[:notice] = t(:notice_successful_delete) else flash[:error] = t(:error_can_not_delete_entry) @@ -100,8 +112,10 @@ def show_local_breadcrumb private - def new_app - @application = ::Doorkeeper::Application.new + def prevent_builtin_edits + if @application.builtin? + render_403 + end end def find_app diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 6ed1a45ae846..05ce7e4b176d 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -552,6 +552,7 @@ def self.permitted_attributes :name, :redirect_uri, :confidential, + :enabled, :client_credentials_user_id, { scopes: [] } ], diff --git a/app/models/user.rb b/app/models/user.rb index ca6f7e0e1924..1c84844dd3ae 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -401,6 +401,10 @@ def wants_comments_in_reverse_order? pref.comments_in_reverse_order? end + def active_admin? + admin? && active? + end + def self.find_by_rss_key(key) return nil unless Setting.feeds_enabled? diff --git a/app/seeders/oauth_applications_seeder.rb b/app/seeders/oauth_applications_seeder.rb new file mode 100644 index 000000000000..dfacbf6b24e5 --- /dev/null +++ b/app/seeders/oauth_applications_seeder.rb @@ -0,0 +1,59 @@ +#-- 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. +#++ +class OAuthApplicationsSeeder < Seeder + def seed_data! + call = create_app + unless call.success? + print_error "Seeding mobile oauth application failed:" + call.errors.full_messages.each do |msg| + print_error " #{msg}" + end + end + end + + def applicable? + Doorkeeper::Application.where(builtin: true).empty? + end + + def not_applicable_message + "No need to seed oauth appplications as they are already present." + end + + def create_app + OAuth::Applications::CreateService + .new(user: User.system) + .call( + enabled: true, + name: "OpenProject Mobile App", + redirect_uri: "urn:ietf:wg:oauth:2.0:oob", + builtin: true, + confidential: false, + uid: "DgJZ7Rat23xHZbcq_nxPg5RUuxljonLCN7V7N7GoBAA" + ) + end +end diff --git a/app/seeders/root_seeder.rb b/app/seeders/root_seeder.rb index 840fe59b85e1..7a4f6abb2c9d 100644 --- a/app/seeders/root_seeder.rb +++ b/app/seeders/root_seeder.rb @@ -70,6 +70,7 @@ def do_seed! # Basic data needs be seeded before anything else. seed_basic_data seed_admin_user + seed_oauth_data seed_demo_data seed_development_data if seed_development_data? seed_plugins_data @@ -132,6 +133,11 @@ def seed_admin_user AdminUserSeeder.new(seed_data).seed! end + def seed_oauth_data + print_status "*** Seeding OAuth applications" + OAuthApplicationsSeeder.new(seed_data).seed! + end + def seed_demo_data print_status "*** Seeding demo data" DemoDataSeeder.new(seed_data).seed! diff --git a/app/services/oauth/applications/create_service.rb b/app/services/oauth/applications/create_service.rb new file mode 100644 index 000000000000..102be06afdcd --- /dev/null +++ b/app/services/oauth/applications/create_service.rb @@ -0,0 +1,37 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OAuth + module Applications + class CreateService < BaseServices::Create + def instance(*) + Doorkeeper::Application.new + end + end + end +end diff --git a/app/services/oauth/applications/delete_service.rb b/app/services/oauth/applications/delete_service.rb new file mode 100644 index 000000000000..430f77977f73 --- /dev/null +++ b/app/services/oauth/applications/delete_service.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OAuth + module Applications + class DeleteService < ::BaseServices::Delete + end + end +end diff --git a/app/contracts/oauth/application_contract.rb b/app/services/oauth/applications/set_attributes_service.rb similarity index 58% rename from app/contracts/oauth/application_contract.rb rename to app/services/oauth/applications/set_attributes_service.rb index 8560edfabc89..9c585cf909c0 100644 --- a/app/contracts/oauth/application_contract.rb +++ b/app/services/oauth/applications/set_attributes_service.rb @@ -27,38 +27,26 @@ #++ module OAuth - class ApplicationContract < ::ModelContract - def self.model - ::Doorkeeper::Application - end - - validate :validate_client_credential_user - validate :validate_integration - - attribute :name - attribute :redirect_uri - attribute :confidential - attribute :owner_id - attribute :owner_type - attribute :scopes - attribute :client_credentials_user_id - attribute :integration_id - attribute :integration_type + module Applications + class SetAttributesService < ::BaseServices::SetAttributes + private - private - - def validate_integration - if (model.integration_id.nil? && model.integration_type.present?) || - (model.integration_id.present? && model.integration_type.nil?) - errors.add :integration, :invalid + def set_default_attributes(*) + model.extend(OpenProject::ChangedBySystem) + model.change_by_system do + set_secret_and_id + set_default_owner unless model.owner_id + end end - end - def validate_client_credential_user - return if model.client_credentials_user_id.blank? + def set_secret_and_id + model.renew_secret if model.secret.blank? + model.uid = Doorkeeper::OAuth::Helpers::UniqueToken.generate if model.uid.blank? + end - unless User.exists?(id: model.client_credentials_user_id) - errors.add :client_credentials_user_id, :invalid + def set_default_owner + model.owner = user + model.owner_type = "User" end end end diff --git a/app/services/oauth/applications/update_service.rb b/app/services/oauth/applications/update_service.rb new file mode 100644 index 000000000000..67d1852be2d7 --- /dev/null +++ b/app/services/oauth/applications/update_service.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module OAuth + module Applications + class UpdateService < ::BaseServices::Update + end + end +end diff --git a/app/views/oauth/applications/_form.html.erb b/app/views/oauth/applications/_form.html.erb index a18661e447ea..3a4f35088666 100644 --- a/app/views/oauth/applications/_form.html.erb +++ b/app/views/oauth/applications/_form.html.erb @@ -30,6 +30,13 @@ See COPYRIGHT and LICENSE files for more details. <%= error_messages_for @application %>
+
+ <%= 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/show.html.erb b/app/views/oauth/applications/show.html.erb index 52d257225a44..cf6dfcc5494a 100644 --- a/app/views/oauth/applications/show.html.erb +++ b/app/views/oauth/applications/show.html.erb @@ -31,6 +31,7 @@ See COPYRIGHT and LICENSE files for more details. <%= toolbar title: "#{t('oauth.application.singular')} - #{h(@application.name)}", title_class: 'no-padding-bottom' do %> + <% unless @application.builtin? %>
  • <%= link_to edit_oauth_application_path(@application), class: 'button' do %> @@ -47,6 +48,7 @@ See COPYRIGHT and LICENSE files for more details. <%= t(:button_delete) %> <% end %>
  • + <% end %> <% end %> <%= render(AttributeGroups::AttributeGroupComponent.new) do |component| %> 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/locales/en.yml b/config/locales/en.yml index 7caf1b92b95b..5af8ea3d62a0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -810,6 +810,8 @@ Project attributes and sections are defined in the The allowed URLs authorized users can be redirected to. One entry per line. diff --git a/config/routes.rb b/config/routes.rb index e5c9878726af..f09f12beed8f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -423,7 +423,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..1c531cc451cc --- /dev/null +++ b/db/migrate/20240513135928_extend_oauth_applications.rb @@ -0,0 +1,6 @@ +class ExtendOAuthApplications < ActiveRecord::Migration[7.1] + def change + add_column :oauth_applications, :builtin, :boolean, default: false + add_column :oauth_applications, :enabled, :boolean, default: true + end +end diff --git a/modules/storages/app/contracts/storages/storages/concerns/manage_storages_guarded.rb b/modules/storages/app/contracts/storages/storages/concerns/manage_storages_guarded.rb index 44eec02b1b8d..d328915166c6 100644 --- a/modules/storages/app/contracts/storages/storages/concerns/manage_storages_guarded.rb +++ b/modules/storages/app/contracts/storages/storages/concerns/manage_storages_guarded.rb @@ -52,7 +52,7 @@ module ManageStoragesGuarded # Small procedure to check that the current user is admin and active def validate_user_allowed_to_manage - unless user.admin? && user.active? + unless user.active_admin? errors.add :base, :error_unauthorized end end 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 ff9a40ef9112..84b7d140c5d1 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 10046c578013..5d5e55d6f70c 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/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index 5ab1af33d4a8..f87b915f0949 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 f1b75ddad8c3..51dc681bed67 100644 --- a/spec/features/admin/oauth/oauth_applications_management_spec.rb +++ b/spec/features/admin/oauth/oauth_applications_management_spec.rb @@ -29,14 +29,15 @@ 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") @@ -91,4 +92,44 @@ # Table is empty again expect(page).to have_css(".generic-table--empty-row", text: "There is currently nothing to display") end + + context "with a seeded application" do + before do + ::OAuthApplicationsSeeder.new.seed_data! + end + + it "does not allow editing or deleting the seeded application" do + visit oauth_applications_path + + expect(page).to have_css("td.name", text: "OpenProject Mobile App") + expect(page).to have_css("td.builtin .icon-checkmark") + expect(page).to have_css("td.enabled .icon-checkmark") + + expect(page).not_to have_css("td.buttons", text: "Edit") + expect(page).not_to have_css("td.buttons", text: "Delete") + + expect(page).to have_css("td.buttons", text: "Deactivate") + click_link_or_button "Deactivate" + + expect(page).to have_css("td.builtin .icon-checkmark") + expect(page).not_to have_css("td.enabled .icon-checkmark") + + app = Doorkeeper::Application.last + expect(app).to be_builtin + expect(app).not_to be_enabled + + expect(page).to have_css("td.buttons", text: "Activate") + click_link_or_button "Activate" + + expect(page).to have_css("td.builtin .icon-checkmark") + expect(page).to have_css("td.enabled .icon-checkmark") + + app.reload + expect(app).to be_builtin + expect(app).to be_enabled + + 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 e6d3db4fcf7f..d55778b1e3b2 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..962113a3e908 --- /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 + it_behaves_like "BaseServices create service" do + let(:factory) { :oauth_application } + let(:model_class) { Doorkeeper::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..dc0d151771e6 --- /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 + 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..5b891f89fc8b --- /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 + 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..eceb08d8a643 --- /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 + it_behaves_like "BaseServices update service" do + let(:factory) { :oauth_application } + end +end