From ec234d2edaa31e71b397d9b1333c63afecd53c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 13 May 2024 18:45:36 +0200 Subject: [PATCH 1/5] [#53620] Add OAuth application flow for builtin mobile app --- .../oauth/applications/row_component.rb | 38 ++++++- .../oauth/applications/table_component.rb | 2 + .../concerns/requires_admin_guard.rb | 2 +- app/contracts/delete_contract.rb | 2 +- .../oauth/applications/base_contract.rb | 75 +++++++++++++ .../oauth/applications/create_contract.rb} | 40 ++----- .../oauth/applications/delete_contract.rb | 35 ++++++ .../oauth/applications/update_contract.rb | 34 ++++++ .../oauth_clients/create_contract.rb | 2 +- .../oauth/applications_controller.rb | 34 ++++-- app/models/permitted_params.rb | 1 + app/models/user.rb | 4 + app/seeders/oauth_applications_seeder.rb | 59 +++++++++++ app/seeders/root_seeder.rb | 6 ++ .../oauth/applications/create_service.rb | 37 +++++++ .../oauth/applications/delete_service.rb | 34 ++++++ .../applications/set_attributes_service.rb} | 44 +++----- .../oauth/applications/update_service.rb | 34 ++++++ app/views/oauth/applications/_form.html.erb | 7 ++ app/views/oauth/applications/show.html.erb | 23 +++- config/initializers/doorkeeper.rb | 5 + config/locales/en.yml | 5 + config/routes.rb | 6 +- ...0240513135928_extend_oauth_applications.rb | 6 ++ .../concerns/manage_storages_guarded.rb | 2 +- .../oauth_applications/create_service.rb | 20 ++-- .../storages/storages/update_service.rb | 12 +-- .../oauth/applications_controller_spec.rb | 24 ++--- .../oauth_applications_management_spec.rb | 45 +++++++- .../oauth/authorization_code_flow_spec.rb | 17 +++ .../oauth/applications/create_service_spec.rb | 39 +++++++ .../oauth/applications/delete_service_spec.rb | 38 +++++++ .../set_attributes_service_spec.rb | 100 ++++++++++++++++++ .../oauth/applications/update_service_spec.rb | 38 +++++++ 34 files changed, 763 insertions(+), 107 deletions(-) create mode 100644 app/contracts/oauth/applications/base_contract.rb rename app/{services/oauth/persist_application_service.rb => contracts/oauth/applications/create_contract.rb} (57%) create mode 100644 app/contracts/oauth/applications/delete_contract.rb create mode 100644 app/contracts/oauth/applications/update_contract.rb create mode 100644 app/seeders/oauth_applications_seeder.rb create mode 100644 app/services/oauth/applications/create_service.rb create mode 100644 app/services/oauth/applications/delete_service.rb rename app/{contracts/oauth/application_contract.rb => services/oauth/applications/set_attributes_service.rb} (58%) create mode 100644 app/services/oauth/applications/update_service.rb create mode 100644 db/migrate/20240513135928_extend_oauth_applications.rb create mode 100644 spec/services/oauth/applications/create_service_spec.rb create mode 100644 spec/services/oauth/applications/delete_service_spec.rb create mode 100644 spec/services/oauth/applications/set_attributes_service_spec.rb create mode 100644 spec/services/oauth/applications/update_service_spec.rb diff --git a/app/components/oauth/applications/row_component.rb b/app/components/oauth/applications/row_component.rb index 840dc2ca1682..2778023eca9f 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 267f82c23ff2..85ef1ef6175a 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 133ac24d0bb7..c4950a783075 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 d8f06edc14e9..6deff8380103 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) when Symbol 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 a33abd8eab60..947cea0ac3c1 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 404436c4a1a9..f8f439372b50 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 e29720877e22..113c86ae3f8c 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) @@ -94,8 +106,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 6507f0ae91dc..944f962afaa4 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -549,6 +549,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 23e85c02b3bf..ef48d89c298d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -413,6 +413,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..aee146695513 --- /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: "openprojectapp://oauth-callback", + builtin: true, + confidential: false, + uid: "DgJZ7Rat23xHZbcq_nxPg5RUuxljonLCN7V7N7GoBAA" + ) + end +end diff --git a/app/seeders/root_seeder.rb b/app/seeders/root_seeder.rb index c1a683ca0f5c..f1a150e4723f 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 46caf0890f8c..72a81dc965f5 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 1bd043582fa3..e0d45a78235e 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 589274cc0a28..bcd963c864e3 100644 --- a/app/views/oauth/applications/show.html.erb +++ b/app/views/oauth/applications/show.html.erb @@ -29,9 +29,30 @@ 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) %> +<%= 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 %> + <%= op_icon('button--icon icon-edit') %> + <%= t(:button_edit) %> + <% end %> +
  • +
  • + <%= link_to oauth_application_path(@application), + method: :delete, + data: { confirm: I18n.t(:text_are_you_sure) }, + class: 'button -danger' do %> + <%= op_icon('button--icon icon-delete') %> + <%= t(:button_delete) %> + <% end %> +
  • + <% end %> +<% end %> + <%= render(AttributeGroups::AttributeGroupComponent.new) do |component| %> <% component.with_header(title: t(:label_details)) %> 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 a06ce1ea8baa..fed2c349fbb2 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" @@ -1402,6 +1404,7 @@ en: limit_reached: You can only do %{limit} backups per day. button_actions: "Actions" + button_activate: "Activate" button_add: "Add" button_add_comment: "Add comment" button_add_member: Add member @@ -1427,6 +1430,7 @@ en: button_copy_and_follow: "Copy and follow" button_create: "Create" button_create_and_continue: "Create and continue" + button_deactivate: "Deactivate" button_delete: "Delete" button_decline: "Decline" button_delete_watcher: "Delete watcher %{name}" @@ -3996,6 +4000,7 @@ en: new: "New OAuth application" 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. diff --git a/config/routes.rb b/config/routes.rb index f153fad2b291..5530fd16f3c1 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..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 34e85ad8d39b..010ba0ab2497 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 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/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..3c69acb22dad 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 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..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 From 8945bcc00a9e3a4d2bb30a372c77e3864740cf42 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Wed, 4 Sep 2024 16:28:44 +0200 Subject: [PATCH 2/5] [#53620] rework of oauth app index page to primer --- .../application_row_component.html.erb | 40 ++++++ .../applications/application_row_component.rb | 43 +++++++ .../applications/index_component.html.erb | 49 ++++++++ .../oauth/applications/index_component.rb | 44 +++++++ .../oauth/applications/row_component.rb | 114 ------------------ .../oauth/applications/table_component.rb | 78 ------------ .../oauth/show_page_header_component.html.erb | 50 ++++---- .../oauth/applications/base_contract.rb | 12 +- .../oauth/applications_controller.rb | 7 +- app/models/user.rb | 4 - app/seeders/oauth_applications_seeder.rb | 10 +- app/views/oauth/applications/index.html.erb | 4 +- app/views/oauth/applications/show.html.erb | 27 +---- config/locales/en.yml | 10 +- ...0240513135928_extend_oauth_applications.rb | 6 +- .../oauth_applications_management_spec.rb | 8 +- .../oauth/applications/create_service_spec.rb | 4 +- .../oauth/applications/delete_service_spec.rb | 2 +- .../set_attributes_service_spec.rb | 2 +- .../oauth/applications/update_service_spec.rb | 2 +- 20 files changed, 246 insertions(+), 270 deletions(-) create mode 100644 app/components/oauth/applications/application_row_component.html.erb create mode 100644 app/components/oauth/applications/application_row_component.rb create mode 100644 app/components/oauth/applications/index_component.html.erb create mode 100644 app/components/oauth/applications/index_component.rb delete mode 100644 app/components/oauth/applications/row_component.rb delete mode 100644 app/components/oauth/applications/table_component.rb diff --git a/app/components/oauth/applications/application_row_component.html.erb b/app/components/oauth/applications/application_row_component.html.erb new file mode 100644 index 000000000000..5813d4cd4aab --- /dev/null +++ b/app/components/oauth/applications/application_row_component.html.erb @@ -0,0 +1,40 @@ +<%= + component_wrapper do + flex_layout(align_items: :center, justify_content: :space_between) do |oauth_application_container| + oauth_application_container.with_column(flex_layout: true) do |application_information| + application_information.with_column(mr: 2) do + render(Primer::Beta::Link.new(href: oauth_application_path(@application), font_weight: :bold)) do + @application.name + end + end + + application_information.with_column(mr: 2) do + render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do + if @application.builtin? + t("oauth.application.builtin") + else + t(:label_created_by, user: @application.owner) + end + end + end + + unless @application.builtin? + application_information.with_column do + render(Primer::Beta::Label.new) do + @application.confidential? ? t("oauth.application.confidential") : t("oauth.application.non_confidential") + end + end + end + end + + # Actions + oauth_application_container.with_column do + render(Primer::Alpha::ToggleSwitch.new( + src: toggle_oauth_application_path(@application), + csrf_token: form_authenticity_token, + checked: @application.enabled? + )) + end + end + end +%> diff --git a/app/components/oauth/applications/application_row_component.rb b/app/components/oauth/applications/application_row_component.rb new file mode 100644 index 000000000000..8ee4387de717 --- /dev/null +++ b/app/components/oauth/applications/application_row_component.rb @@ -0,0 +1,43 @@ +#-- 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 OAuth + module Applications + class ApplicationRowComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(application:) + @application = application + + super + end + end + end +end diff --git a/app/components/oauth/applications/index_component.html.erb b/app/components/oauth/applications/index_component.html.erb new file mode 100644 index 000000000000..020862cae064 --- /dev/null +++ b/app/components/oauth/applications/index_component.html.erb @@ -0,0 +1,49 @@ +<%= + component_wrapper do + flex_layout do |index_container| + index_container.with_row do + render(border_box_container(mb: 4)) do |component| + component.with_header(font_weight: :bold) do + render(Primer::Beta::Text.new) do + t("oauth.header.builtin_applications") + end + end + + if @built_in_applications.empty? + component.with_row do + render(Primer::Beta::Text.new) do + t("oauth.empty_application_lists") + end + end + end + + @built_in_applications.each do |application| + component.with_row { render(OAuth::Applications::ApplicationRowComponent.new(application:)) } + end + end + end + + index_container.with_row do + render(border_box_container(mb: 4)) do |component| + component.with_header(font_weight: :bold) do + render(Primer::Beta::Text.new) do + t("oauth.header.other_applications") + end + end + + if @other_applications.empty? + component.with_row do + render(Primer::Beta::Text.new) do + t("oauth.empty_application_lists") + end + end + end + + @other_applications.each do |application| + component.with_row { render(OAuth::Applications::ApplicationRowComponent.new(application:)) } + end + end + end + end + end +%> diff --git a/app/components/oauth/applications/index_component.rb b/app/components/oauth/applications/index_component.rb new file mode 100644 index 000000000000..81027eea51f3 --- /dev/null +++ b/app/components/oauth/applications/index_component.rb @@ -0,0 +1,44 @@ +#-- 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 OAuth + module Applications + class IndexComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(oauth_applications:) + @built_in_applications = oauth_applications.select(&:builtin?) + @other_applications = oauth_applications.reject(&:builtin?) + + super + end + end + end +end diff --git a/app/components/oauth/applications/row_component.rb b/app/components/oauth/applications/row_component.rb deleted file mode 100644 index 2778023eca9f..000000000000 --- a/app/components/oauth/applications/row_component.rb +++ /dev/null @@ -1,114 +0,0 @@ -# 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 OAuth - module Applications - class RowComponent < ::RowComponent - property :confidential - - def application - model - end - - def builtin - checkmark(application.builtin?) - end - - def enabled - checkmark(application.enabled?) - end - - def name - if enabled - link_to application.name, oauth_application_path(application) - else - render(Primer::Beta::Text.new(color: :muted)) { application.name } - end - end - - def owner - link_to application.owner.name, user_path(application.owner) - end - - def confidential - if application.confidential? - helpers.op_icon "icon icon-checkmark" - end - end - - def redirect_uri - urls = application.redirect_uri.split("\n") - safe_join urls, "
    ".html_safe - end - - def client_credentials - if user_id = application.client_credentials_user_id - helpers.link_to_user User.find(user_id) - else - "-" - end - end - - def edit_link - link_to( - I18n.t(:button_edit), - edit_oauth_application_path(application), - class: "oauth-application--edit-link icon icon-edit" - ) - end - - def button_links - 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 -end diff --git a/app/components/oauth/applications/table_component.rb b/app/components/oauth/applications/table_component.rb deleted file mode 100644 index 85ef1ef6175a..000000000000 --- a/app/components/oauth/applications/table_component.rb +++ /dev/null @@ -1,78 +0,0 @@ -# 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 OAuth - module Applications - class TableComponent < ::TableComponent - class << self - def row_class - ::OAuth::Applications::RowComponent - end - end - - def initial_sort - %i[id asc] - end - - def sortable? - false - end - - def columns - headers.map(&:first) - end - - def inline_create_link - link_to new_oauth_application_path, - aria: { label: t("oauth.application.new") }, - class: "wp-inline-create--add-link", - title: t("oauth.application.new") do - helpers.op_icon("icon icon-add") - end - end - - def empty_row_message - I18n.t :no_results_title_text - end - - 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) }] - ] - end - end - end -end diff --git a/app/components/oauth/show_page_header_component.html.erb b/app/components/oauth/show_page_header_component.html.erb index d340f870abcb..8bb3d06627e8 100644 --- a/app/components/oauth/show_page_header_component.html.erb +++ b/app/components/oauth/show_page_header_component.html.erb @@ -32,31 +32,33 @@ See COPYRIGHT and LICENSE files for more details. header.with_title { h(@application.name) } header.with_breadcrumbs(breadcrumb_items) - header.with_action_button(tag: :a, - mobile_icon: :pencil, - mobile_label: t(:button_edit), - size: :medium, - href: edit_oauth_application_path(@application), - aria: { label: I18n.t(:button_edit) }, - title: I18n.t(:button_edit)) do |button| - button.with_leading_visual_icon(icon: :pencil) - t(:button_edit) - end + unless @application.builtin? + header.with_action_button(tag: :a, + mobile_icon: :pencil, + mobile_label: t(:button_edit), + size: :medium, + href: edit_oauth_application_path(@application), + aria: { label: I18n.t(:button_edit) }, + title: I18n.t(:button_edit)) do |button| + button.with_leading_visual_icon(icon: :pencil) + t(:button_edit) + end - header.with_action_button(tag: :a, - scheme: :danger, - mobile_icon: :trash, - mobile_label: t(:button_delete), - size: :medium, - href: oauth_application_path(@application), - aria: { label: I18n.t(:button_delete) }, - data: { - confirm: I18n.t(:text_are_you_sure), - method: :delete - }, - title: I18n.t(:button_delete)) do |button| - button.with_leading_visual_icon(icon: :trash) - t(:button_delete) + header.with_action_button(tag: :a, + scheme: :danger, + mobile_icon: :trash, + mobile_label: t(:button_delete), + size: :medium, + href: oauth_application_path(@application), + aria: { label: I18n.t(:button_delete) }, + data: { + confirm: I18n.t(:text_are_you_sure), + method: :delete + }, + title: I18n.t(:button_delete)) do |button| + button.with_leading_visual_icon(icon: :trash) + t(:button_delete) + end end end %> diff --git a/app/contracts/oauth/applications/base_contract.rb b/app/contracts/oauth/applications/base_contract.rb index 4016cfffb51c..fac8b49b1873 100644 --- a/app/contracts/oauth/applications/base_contract.rb +++ b/app/contracts/oauth/applications/base_contract.rb @@ -51,16 +51,14 @@ def self.model private def validate_admin_only - unless user.active_admin? - errors.add :base, :error_unauthorized - end + errors.add :base, :error_unauthorized unless user.admin? 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 + both = model.integration_id.present? && model.integration_type.present? + none = model.integration_id.nil? && model.integration_type.nil? + + errors.add :integration, :invalid unless both || none end def validate_client_credential_user diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 113c86ae3f8c..0d32225d584b 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -53,13 +53,14 @@ def edit; end def create call = ::OAuth::Applications::CreateService.new(user: current_user) .call(permitted_params.oauth_application) + result = call.result if call.success? flash[:notice] = t(:notice_successful_create) - flash[:_application_secret] = call.result.plaintext_secret - redirect_to action: :show, id: call.result.id + flash[:_application_secret] = result.plaintext_secret + redirect_to action: :show, id: result.id else - @application = call.result + @application = result render action: :new end end diff --git a/app/models/user.rb b/app/models/user.rb index ef48d89c298d..23e85c02b3bf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -413,10 +413,6 @@ 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 index aee146695513..b8f88606db18 100644 --- a/app/seeders/oauth_applications_seeder.rb +++ b/app/seeders/oauth_applications_seeder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2024 the OpenProject GmbH @@ -26,6 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ class OAuthApplicationsSeeder < Seeder + OPENPROJECT_MOBILE_APP_UID = "DgJZ7Rat23xHZbcq_nxPg5RUuxljonLCN7V7N7GoBAA" + def seed_data! call = create_app unless call.success? @@ -37,11 +41,11 @@ def seed_data! end def applicable? - Doorkeeper::Application.where(builtin: true).empty? + Doorkeeper::Application.find_by(id: OPENPROJECT_MOBILE_APP_UID).nil? end def not_applicable_message - "No need to seed oauth appplications as they are already present." + "No need to seed oauth applications as they are already present." end def create_app @@ -53,7 +57,7 @@ def create_app redirect_uri: "openprojectapp://oauth-callback", builtin: true, confidential: false, - uid: "DgJZ7Rat23xHZbcq_nxPg5RUuxljonLCN7V7N7GoBAA" + uid: OPENPROJECT_MOBILE_APP_UID ) end end 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 bcd963c864e3..c2f07ed2595d 100644 --- a/app/views/oauth/applications/show.html.erb +++ b/app/views/oauth/applications/show.html.erb @@ -31,28 +31,6 @@ See COPYRIGHT and LICENSE files for more details. <%= render OAuth::ShowPageHeaderComponent.new(application: @application) %> -<%= 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 %> - <%= op_icon('button--icon icon-edit') %> - <%= t(:button_edit) %> - <% end %> -
  • -
  • - <%= link_to oauth_application_path(@application), - method: :delete, - data: { confirm: I18n.t(:text_are_you_sure) }, - class: 'button -danger' do %> - <%= op_icon('button--icon icon-delete') %> - <%= t(:button_delete) %> - <% end %> -
  • - <% end %> -<% end %> - <%= render(AttributeGroups::AttributeGroupComponent.new) do |component| %> <% component.with_header(title: t(:label_details)) %> @@ -93,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/locales/en.yml b/config/locales/en.yml index fed2c349fbb2..81187cf64518 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1404,7 +1404,6 @@ en: limit_reached: You can only do %{limit} backups per day. button_actions: "Actions" - button_activate: "Activate" button_add: "Add" button_add_comment: "Add comment" button_add_member: Add member @@ -1430,7 +1429,6 @@ en: button_copy_and_follow: "Copy and follow" button_create: "Create" button_create_and_continue: "Create and continue" - button_deactivate: "Deactivate" button_delete: "Delete" button_decline: "Decline" button_delete_watcher: "Delete watcher %{name}" @@ -2147,6 +2145,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" @@ -3994,10 +3993,13 @@ 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." @@ -4011,6 +4013,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/db/migrate/20240513135928_extend_oauth_applications.rb b/db/migrate/20240513135928_extend_oauth_applications.rb index 1c531cc451cc..18ec11c1fe37 100644 --- a/db/migrate/20240513135928_extend_oauth_applications.rb +++ b/db/migrate/20240513135928_extend_oauth_applications.rb @@ -1,6 +1,8 @@ class ExtendOAuthApplications < ActiveRecord::Migration[7.1] def change - add_column :oauth_applications, :builtin, :boolean, default: false - add_column :oauth_applications, :enabled, :boolean, default: true + 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/spec/features/admin/oauth/oauth_applications_management_spec.rb b/spec/features/admin/oauth/oauth_applications_management_spec.rb index 3c69acb22dad..149c750943cc 100644 --- a/spec/features/admin/oauth/oauth_applications_management_spec.rb +++ b/spec/features/admin/oauth/oauth_applications_management_spec.rb @@ -95,7 +95,7 @@ context "with a seeded application" do before do - ::OAuthApplicationsSeeder.new.seed_data! + OAuthApplicationsSeeder.new.seed_data! end it "does not allow editing or deleting the seeded application" do @@ -105,14 +105,14 @@ 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_no_css("td.buttons", text: "Edit") + expect(page).to have_no_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") + expect(page).to have_no_css("td.enabled .icon-checkmark") app = Doorkeeper::Application.last expect(app).to be_builtin diff --git a/spec/services/oauth/applications/create_service_spec.rb b/spec/services/oauth/applications/create_service_spec.rb index 962113a3e908..bc299a08502e 100644 --- a/spec/services/oauth/applications/create_service_spec.rb +++ b/spec/services/oauth/applications/create_service_spec.rb @@ -31,9 +31,9 @@ require "spec_helper" require "services/base_services/behaves_like_create_service" -RSpec.describe OAuth::Applications::CreateService, type: :model do +RSpec.describe OAuth::Applications::CreateService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat it_behaves_like "BaseServices create service" do - let(:factory) { :oauth_application } 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 index dc0d151771e6..4fcb9b55fcf0 100644 --- a/spec/services/oauth/applications/delete_service_spec.rb +++ b/spec/services/oauth/applications/delete_service_spec.rb @@ -31,7 +31,7 @@ require "spec_helper" require "services/base_services/behaves_like_delete_service" -RSpec.describe OAuth::Applications::DeleteService, type: :model do +RSpec.describe OAuth::Applications::DeleteService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat it_behaves_like "BaseServices delete service" do let(:factory) { :oauth_application } end diff --git a/spec/services/oauth/applications/set_attributes_service_spec.rb b/spec/services/oauth/applications/set_attributes_service_spec.rb index 5b891f89fc8b..396522209ceb 100644 --- a/spec/services/oauth/applications/set_attributes_service_spec.rb +++ b/spec/services/oauth/applications/set_attributes_service_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" -RSpec.describe OAuth::Applications::SetAttributesService, type: :model do +RSpec.describe OAuth::Applications::SetAttributesService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat let(:current_user) { build_stubbed(:admin) } let(:contract_instance) do diff --git a/spec/services/oauth/applications/update_service_spec.rb b/spec/services/oauth/applications/update_service_spec.rb index eceb08d8a643..a8523eb94f1e 100644 --- a/spec/services/oauth/applications/update_service_spec.rb +++ b/spec/services/oauth/applications/update_service_spec.rb @@ -31,7 +31,7 @@ require "spec_helper" require "services/base_services/behaves_like_update_service" -RSpec.describe OAuth::Applications::UpdateService, type: :model do +RSpec.describe OAuth::Applications::UpdateService, type: :model do # rubocop:disable RSpec/SpecFilePathFormat it_behaves_like "BaseServices update service" do let(:factory) { :oauth_application } end From 44f21eec4ae6da38c6db8c883114873b788adc6a Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Fri, 6 Sep 2024 16:10:38 +0200 Subject: [PATCH 3/5] [#53620] added unit test for oauth application contracts --- .../oauth/applications/delete_contract.rb | 2 +- .../oauth/applications/update_contract.rb | 7 ++ .../oauth/applications/base_contract_spec.rb | 96 +++++++++++++++++++ .../applications/create_contract_spec.rb | 50 ++++++++++ .../applications/delete_contract_spec.rb | 50 ++++++++++ .../oauth/applications/shared_examples.rb | 37 +++++++ .../applications/update_contract_spec.rb | 43 +++++++++ 7 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 spec/contracts/oauth/applications/base_contract_spec.rb create mode 100644 spec/contracts/oauth/applications/create_contract_spec.rb create mode 100644 spec/contracts/oauth/applications/delete_contract_spec.rb create mode 100644 spec/contracts/oauth/applications/shared_examples.rb create mode 100644 spec/contracts/oauth/applications/update_contract_spec.rb diff --git a/app/contracts/oauth/applications/delete_contract.rb b/app/contracts/oauth/applications/delete_contract.rb index ba4c2368d031..1d7ce6f5dab3 100644 --- a/app/contracts/oauth/applications/delete_contract.rb +++ b/app/contracts/oauth/applications/delete_contract.rb @@ -29,7 +29,7 @@ module OAuth module Applications class DeleteContract < ::DeleteContract - delete_permission -> { !model.builtin? && user.active_admin? } + delete_permission -> { !model.builtin? && user.admin? } end end end diff --git a/app/contracts/oauth/applications/update_contract.rb b/app/contracts/oauth/applications/update_contract.rb index 7c4cd202483f..48f436d14fef 100644 --- a/app/contracts/oauth/applications/update_contract.rb +++ b/app/contracts/oauth/applications/update_contract.rb @@ -29,6 +29,13 @@ module OAuth module Applications class UpdateContract < BaseContract + validate :application_is_not_builtin + + def application_is_not_builtin + if model.builtin? + errors.add(:base, :unchangeable) + end + end end end 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 From 820c4abbcc0e6707d0663f8322b1afd5209f013e Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Wed, 11 Sep 2024 13:42:26 +0200 Subject: [PATCH 4/5] [#53620] fixed failing tests --- .../application_row_component.html.erb | 7 ++- .../applications/index_component.html.erb | 10 ++-- .../concerns/requires_admin_guard.rb | 2 +- app/contracts/delete_contract.rb | 2 +- .../oauth_clients/create_contract.rb | 2 +- .../features/bcf/api_authorization_spec.rb | 2 +- .../concerns/manage_storages_guarded.rb | 2 +- .../oauth_applications_management_spec.rb | 48 ++++++++++--------- 8 files changed, 42 insertions(+), 33 deletions(-) diff --git a/app/components/oauth/applications/application_row_component.html.erb b/app/components/oauth/applications/application_row_component.html.erb index 5813d4cd4aab..0a7e9787336a 100644 --- a/app/components/oauth/applications/application_row_component.html.erb +++ b/app/components/oauth/applications/application_row_component.html.erb @@ -1,5 +1,5 @@ <%= - component_wrapper do + component_wrapper(data: { 'test-selector': "op-admin-oauth--application" }) do flex_layout(align_items: :center, justify_content: :space_between) do |oauth_application_container| oauth_application_container.with_column(flex_layout: true) do |application_information| application_information.with_column(mr: 2) do @@ -32,7 +32,10 @@ render(Primer::Alpha::ToggleSwitch.new( src: toggle_oauth_application_path(@application), csrf_token: form_authenticity_token, - checked: @application.enabled? + checked: @application.enabled?, + data: { + 'test-selector': "op-admin-oauth--application-enabled-toggle-switch" + } )) end end diff --git a/app/components/oauth/applications/index_component.html.erb b/app/components/oauth/applications/index_component.html.erb index 020862cae064..856ff7b0b136 100644 --- a/app/components/oauth/applications/index_component.html.erb +++ b/app/components/oauth/applications/index_component.html.erb @@ -2,7 +2,9 @@ component_wrapper do flex_layout do |index_container| index_container.with_row do - render(border_box_container(mb: 4)) do |component| + render(border_box_container(mb: 4, data: { + 'test-selector': "op-admin-oauth--built-in-applications" + })) do |component| component.with_header(font_weight: :bold) do render(Primer::Beta::Text.new) do t("oauth.header.builtin_applications") @@ -11,7 +13,9 @@ if @built_in_applications.empty? component.with_row do - render(Primer::Beta::Text.new) do + render(Primer::Beta::Text.new(data: { + 'test-selector': "op-admin-oauth--built-in-applications-placeholder" + })) do t("oauth.empty_application_lists") end end @@ -33,7 +37,7 @@ if @other_applications.empty? component.with_row do - render(Primer::Beta::Text.new) do + render(Primer::Beta::Text.new(data: { 'test-selector': "op-admin-oauth--applications-placeholder" })) do t("oauth.empty_application_lists") end end diff --git a/app/contracts/concerns/requires_admin_guard.rb b/app/contracts/concerns/requires_admin_guard.rb index c4950a783075..133ac24d0bb7 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.active_admin? + unless user.admin? && user.active? errors.add :base, :error_unauthorized end end diff --git a/app/contracts/delete_contract.rb b/app/contracts/delete_contract.rb index 6deff8380103..d8f06edc14e9 100644 --- a/app/contracts/delete_contract.rb +++ b/app/contracts/delete_contract.rb @@ -56,7 +56,7 @@ def authorized? case permission when :admin - user.active_admin? + user.admin? && user.active? when Proc instance_exec(&permission) when Symbol diff --git a/app/contracts/oauth_clients/create_contract.rb b/app/contracts/oauth_clients/create_contract.rb index f8f439372b50..404436c4a1a9 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.active_admin? + unless user.admin? && user.active? errors.add :base, :error_unauthorized 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/contracts/storages/storages/concerns/manage_storages_guarded.rb b/modules/storages/app/contracts/storages/storages/concerns/manage_storages_guarded.rb index 010ba0ab2497..34e85ad8d39b 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.active_admin? + unless user.admin? && user.active? errors.add :base, :error_unauthorized 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 149c750943cc..faaab28a4884 100644 --- a/spec/features/admin/oauth/oauth_applications_management_spec.rb +++ b/spec/features/admin/oauth/oauth_applications_management_spec.rb @@ -39,7 +39,7 @@ 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 @@ -52,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" @@ -78,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") @@ -90,7 +90,7 @@ 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" do @@ -101,32 +101,34 @@ 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") + app = Doorkeeper::Application.last - expect(page).to have_no_css("td.buttons", text: "Edit") - expect(page).to have_no_css("td.buttons", text: "Delete") + 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") - expect(page).to have_css("td.buttons", text: "Deactivate") - click_link_or_button "Deactivate" + 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") - expect(page).to have_css("td.builtin .icon-checkmark") - expect(page).to have_no_css("td.enabled .icon-checkmark") + app.reload + expect(app).to be_builtin + expect(app).not_to be_enabled - app = Doorkeeper::Application.last - 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") - expect(page).to have_css("td.buttons", text: "Activate") - click_link_or_button "Activate" + app.reload + expect(app).to be_builtin + expect(app).to be_enabled - expect(page).to have_css("td.builtin .icon-checkmark") - expect(page).to have_css("td.enabled .icon-checkmark") + click_on "OpenProject Mobile App" + end - app.reload - expect(app).to be_builtin - expect(app).to be_enabled + 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." From cf4c7c37fd0ff5afa9d8e18ad56648854faec331 Mon Sep 17 00:00:00 2001 From: Eric Schubert Date: Fri, 13 Sep 2024 13:09:45 +0200 Subject: [PATCH 5/5] [#53620] add feature flag for buit-in apps --- .../applications/index_component.html.erb | 36 ++++++++++--------- config/initializers/feature_decisions.rb | 3 ++ .../oauth_applications_management_spec.rb | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/components/oauth/applications/index_component.html.erb b/app/components/oauth/applications/index_component.html.erb index 856ff7b0b136..e5f0d0b4beed 100644 --- a/app/components/oauth/applications/index_component.html.erb +++ b/app/components/oauth/applications/index_component.html.erb @@ -1,28 +1,30 @@ <%= component_wrapper do flex_layout do |index_container| - index_container.with_row do - render(border_box_container(mb: 4, data: { - 'test-selector': "op-admin-oauth--built-in-applications" - })) do |component| - component.with_header(font_weight: :bold) do - render(Primer::Beta::Text.new) do - t("oauth.header.builtin_applications") + if OpenProject::FeatureDecisions.built_in_oauth_applications_active? + index_container.with_row do + render(border_box_container(mb: 4, data: { + 'test-selector': "op-admin-oauth--built-in-applications" + })) do |component| + component.with_header(font_weight: :bold) do + render(Primer::Beta::Text.new) do + t("oauth.header.builtin_applications") + end end - end - if @built_in_applications.empty? - component.with_row do - render(Primer::Beta::Text.new(data: { - 'test-selector': "op-admin-oauth--built-in-applications-placeholder" - })) do - t("oauth.empty_application_lists") + if @built_in_applications.empty? + component.with_row do + render(Primer::Beta::Text.new(data: { + 'test-selector': "op-admin-oauth--built-in-applications-placeholder" + })) do + t("oauth.empty_application_lists") + end end end - end - @built_in_applications.each do |application| - component.with_row { render(OAuth::Applications::ApplicationRowComponent.new(application:)) } + @built_in_applications.each do |application| + component.with_row { render(OAuth::Applications::ApplicationRowComponent.new(application:)) } + end end end end 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/spec/features/admin/oauth/oauth_applications_management_spec.rb b/spec/features/admin/oauth/oauth_applications_management_spec.rb index faaab28a4884..fd7c4bbbb1c6 100644 --- a/spec/features/admin/oauth/oauth_applications_management_spec.rb +++ b/spec/features/admin/oauth/oauth_applications_management_spec.rb @@ -93,7 +93,7 @@ expect(page).to have_test_selector("op-admin-oauth--applications-placeholder") end - context "with a seeded application" do + context "with a seeded application", with_flag: { built_in_oauth_applications: true } do before do OAuthApplicationsSeeder.new.seed_data! end