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