diff --git a/app/components/admin/scim_clients/client_id_component.html.erb b/app/components/admin/scim_clients/client_id_component.html.erb new file mode 100644 index 00000000000..9ed5312bce6 --- /dev/null +++ b/app/components/admin/scim_clients/client_id_component.html.erb @@ -0,0 +1,45 @@ +<%#-- 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. + +++#%> + +<%= + render(Primer::OpenProject::InputGroup.new(input_width: :large)) do |input_group| + input_group.with_text_input( + name: :client_id, + label: Doorkeeper::Application.human_attribute_name(:uid), + visually_hide_label: false, + value: model.oauth_application.uid + ) + input_group.with_trailing_action_clipboard_copy_button( + value: model.oauth_application.uid, + aria: { + label: I18n.t("button_copy_to_clipboard") + } + ) + end +%> diff --git a/app/components/admin/scim_clients/client_id_component.rb b/app/components/admin/scim_clients/client_id_component.rb new file mode 100644 index 00000000000..2a578d1df4e --- /dev/null +++ b/app/components/admin/scim_clients/client_id_component.rb @@ -0,0 +1,36 @@ +# 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 Admin + module ScimClients + class ClientIdComponent < ApplicationComponent + end + end +end diff --git a/app/components/admin/scim_clients/created_client_credentials_dialog_component.html.erb b/app/components/admin/scim_clients/created_client_credentials_dialog_component.html.erb new file mode 100644 index 00000000000..b5f65d89161 --- /dev/null +++ b/app/components/admin/scim_clients/created_client_credentials_dialog_component.html.erb @@ -0,0 +1,72 @@ +<%#-- 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. + +++#%> + +<%= render(Primer::OpenProject::FeedbackDialog.new(title: t(".title"), size: :large, classes: "Overlay--size-large-portrait", **system_arguments)) do |dialog| + dialog.with_feedback_message(icon_arguments: { icon: :"check-circle" }) do |message| + message.with_heading(tag: :h2).with_content(t(".heading")) + end + + dialog.with_additional_details(display: :block) do + render(Primer::OpenProject::FlexLayout.new) do |layout| + layout.with_row do + render(Primer::OpenProject::InputGroup.new) do |input_group| + input_group.with_text_input( + name: :client_id, + label: Doorkeeper::Application.human_attribute_name(:uid), + visually_hide_label: false, + value: model.oauth_application.uid + ) + input_group.with_trailing_action_clipboard_copy_button( + value: model.oauth_application.uid, + aria: { label: I18n.t("button_copy_to_clipboard") } + ) + end + end + + layout.with_row do + render(Primer::OpenProject::InputGroup.new) do |input_group| + input_group.with_text_input( + name: :client_secret, + label: Doorkeeper::Application.human_attribute_name(:secret), + visually_hide_label: false, + value: model.oauth_application.secret + ) + input_group.with_trailing_action_clipboard_copy_button( + value: model.oauth_application.secret, + aria: { label: I18n.t("button_copy_to_clipboard") } + ) + end + end + + layout.with_row(mt: 3) do + render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert)) { t(".one_time_hint") } + end + end + end +end %> diff --git a/app/components/admin/scim_clients/created_client_credentials_dialog_component.rb b/app/components/admin/scim_clients/created_client_credentials_dialog_component.rb new file mode 100644 index 00000000000..73d753d5ff9 --- /dev/null +++ b/app/components/admin/scim_clients/created_client_credentials_dialog_component.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. +#++ + +module Admin + module ScimClients + class CreatedClientCredentialsDialogComponent < ApplicationComponent + include OpTurbo::Streamable + + TEST_SELECTOR = "op-scim-clients--created-client-credentials-dialog" + + def system_arguments + options + end + end + end +end diff --git a/app/components/admin/scim_clients/created_token_dialog_component.html.erb b/app/components/admin/scim_clients/created_token_dialog_component.html.erb new file mode 100644 index 00000000000..0d0487df0ba --- /dev/null +++ b/app/components/admin/scim_clients/created_token_dialog_component.html.erb @@ -0,0 +1,53 @@ +<%#-- 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. + +++#%> + +<%= render(Primer::OpenProject::FeedbackDialog.new(title: t(".title"), size: :large, **system_arguments)) do |dialog| + dialog.with_feedback_message(icon_arguments: { icon: :"check-circle" }) do |message| + message.with_heading(tag: :h2).with_content(t(".heading")) + end + + dialog.with_additional_details(display: :block) do + concat( + render(Primer::OpenProject::InputGroup.new) do |input_group| + input_group.with_text_input( + name: :token, + label: t(".label_token"), + visually_hide_label: false, + value: model.token + ) + input_group.with_trailing_action_clipboard_copy_button( + value: model.token, + aria: { label: I18n.t("button_copy_to_clipboard") } + ) + end + ) + + concat(render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert, mt: 3)) { t(".one_time_hint") }) + end +end %> diff --git a/app/components/admin/scim_clients/created_token_dialog_component.rb b/app/components/admin/scim_clients/created_token_dialog_component.rb new file mode 100644 index 00000000000..533f16df11c --- /dev/null +++ b/app/components/admin/scim_clients/created_token_dialog_component.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. +#++ + +module Admin + module ScimClients + class CreatedTokenDialogComponent < ApplicationComponent + include OpTurbo::Streamable + + TEST_SELECTOR = "op-scim-clients--created-static-token-dialog" + + def system_arguments + options + end + end + end +end diff --git a/app/components/admin/scim_clients/delete_scim_client_dialog_component.html.erb b/app/components/admin/scim_clients/delete_scim_client_dialog_component.html.erb new file mode 100644 index 00000000000..b138dc18ae6 --- /dev/null +++ b/app/components/admin/scim_clients/delete_scim_client_dialog_component.html.erb @@ -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. + +++#%> + +<%= + render( + Primer::OpenProject::DangerDialog.new( + title: t(".title"), + form_arguments:, + test_selector: TEST_SELECTOR + ) + ) do |dialog| + dialog.with_confirmation_message do |message| + message.with_heading(tag: :h2) { t(".heading") } + message.with_description_content(t(".description")) + end + end +%> diff --git a/app/components/admin/scim_clients/delete_scim_client_dialog_component.rb b/app/components/admin/scim_clients/delete_scim_client_dialog_component.rb new file mode 100644 index 00000000000..3816a4ae862 --- /dev/null +++ b/app/components/admin/scim_clients/delete_scim_client_dialog_component.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Admin + module ScimClients + class DeleteScimClientDialogComponent < ApplicationComponent + include OpTurbo::Streamable + + TEST_SELECTOR = "op-scim-clients--delete-client-dialog" + + def form_arguments + { + action: admin_scim_client_path(model), + method: :delete + } + end + end + end +end diff --git a/app/components/admin/scim_clients/form_component.html.erb b/app/components/admin/scim_clients/form_component.html.erb new file mode 100644 index 00000000000..c2ff66357e9 --- /dev/null +++ b/app/components/admin/scim_clients/form_component.html.erb @@ -0,0 +1,36 @@ +<%#-- 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. + +++#%> + +<%= + component_wrapper(tag: "turbo-frame") do + settings_primer_form_with(**form_options) do |f| + render(ScimClients::Form.new(f)) + end + end +%> diff --git a/app/components/admin/scim_clients/form_component.rb b/app/components/admin/scim_clients/form_component.rb new file mode 100644 index 00000000000..68b9719b1c8 --- /dev/null +++ b/app/components/admin/scim_clients/form_component.rb @@ -0,0 +1,63 @@ +# 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 Admin::ScimClients + class FormComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def self.wrapper_key = :scim_clients_form + + private + + def form_options + form_target.merge(stimulus_controller_options) + .merge(model:) + end + + def form_target + if model.new_record? + { method: :post, url: admin_scim_clients_path } + else + { method: :patch, url: admin_scim_client_path(model) } + end + end + + def stimulus_controller_options + { + data: { + controller: "scim-clients--form-inputs", + turbo_frame: "_top" + } + } + end + end +end diff --git a/app/components/admin/scim_clients/revoke_static_token_dialog_component.html.erb b/app/components/admin/scim_clients/revoke_static_token_dialog_component.html.erb new file mode 100644 index 00000000000..d99ee402947 --- /dev/null +++ b/app/components/admin/scim_clients/revoke_static_token_dialog_component.html.erb @@ -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. + +++#%> + +<%= + render( + Primer::OpenProject::DangerDialog.new( + title: t(".title"), + confirm_button_text: t(".confirm_button"), + form_arguments:, + test_selector: TEST_SELECTOR + ) + ) do |dialog| + dialog.with_confirmation_message do |message| + message.with_heading(tag: :h2) { t(".heading") } + message.with_description_content(t(".description")) + end + end +%> diff --git a/app/models/scim_clients/form_model.rb b/app/components/admin/scim_clients/revoke_static_token_dialog_component.rb similarity index 64% rename from app/models/scim_clients/form_model.rb rename to app/components/admin/scim_clients/revoke_static_token_dialog_component.rb index 44a154e0942..3fad57875a5 100644 --- a/app/models/scim_clients/form_model.rb +++ b/app/components/admin/scim_clients/revoke_static_token_dialog_component.rb @@ -28,28 +28,31 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ScimClients - FormModel = Data.define(:name, :auth_provider_id, :authentication_method, :jwt_sub) do - extend ActiveModel::Naming +module Admin + module ScimClients + class RevokeStaticTokenDialogComponent < ApplicationComponent + include OpTurbo::Streamable - class << self - def from_client(client) - jwt_sub = client.service_account&.active_user_auth_provider_link&.external_id - new( - name: client.name, - auth_provider_id: client.auth_provider_id, - authentication_method: client.authentication_method, - jwt_sub: - ) + TEST_SELECTOR = "op-scim-clients--revoke-static-token-dialog" + + def initialize(model, scim_client_id:, turbo_frame: nil) + super(model) + + @scim_client_id = scim_client_id + @turbo_frame = turbo_frame end - def from_params(params) - new( - name: params[:name], - auth_provider_id: params[:auth_provider_id], - authentication_method: params[:authentication_method].to_s, - jwt_sub: params[:jwt_sub] - ) + def form_arguments + { + action: admin_scim_client_static_token_path(model, scim_client_id: @scim_client_id), + method: :delete + }.merge(turbo_frame_arguments) + end + + def turbo_frame_arguments + return {} if @turbo_frame.nil? + + { data: { turbo_frame: @turbo_frame } } end end end diff --git a/app/components/admin/scim_clients/row_component.rb b/app/components/admin/scim_clients/row_component.rb new file mode 100644 index 00000000000..59242b4bff6 --- /dev/null +++ b/app/components/admin/scim_clients/row_component.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Admin::ScimClients + class RowComponent < OpPrimer::BorderBoxRowComponent + def name + render(Primer::Beta::Link.new(href: edit_admin_scim_client_path(model), font_weight: :bold)) { model.name } + end + + def user_count + return "—" if model.auth_provider.nil? + + model.auth_provider.users.count + end + + def authentication_method + t("admin.scim_clients.authentication_methods.#{model.authentication_method}") + end + + def created_at + helpers.format_date(model.created_at) + end + end +end diff --git a/app/components/admin/scim_clients/table_component.rb b/app/components/admin/scim_clients/table_component.rb new file mode 100644 index 00000000000..d6b17a514d3 --- /dev/null +++ b/app/components/admin/scim_clients/table_component.rb @@ -0,0 +1,65 @@ +# 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 Admin::ScimClients + class TableComponent < OpPrimer::BorderBoxTableComponent + columns :name, :user_count, :authentication_method, :created_at + mobile_labels :user_count, :authentication_method, :created_at + + def mobile_title + ScimClient.model_name.human(count: 2) + end + + def row_class + RowComponent + end + + def headers + [ + [:name, { caption: ScimClient.human_attribute_name(:name) }], + [:user_count, { caption: t(".user_count") }], + [:authentication_method, { caption: ScimClient.human_attribute_name(:authentication_method) }], + [:created_at, { caption: ScimClient.human_attribute_name(:created_at) }] + ] + end + + def blank_title + t(".blank_slate.title") + end + + def blank_description + t(".blank_slate.description") + end + + def blank_icon + :key + end + end +end diff --git a/app/components/admin/scim_clients/token_list_component.html.erb b/app/components/admin/scim_clients/token_list_component.html.erb new file mode 100644 index 00000000000..c2556667bcc --- /dev/null +++ b/app/components/admin/scim_clients/token_list_component.html.erb @@ -0,0 +1,52 @@ +<%#-- 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. + +++#%> + +<%= component_wrapper(tag: "turbo-frame") do %> + <%= render(Primer::Beta::Subhead.new(spacious: true)) do |component| + component.with_heading { t(".heading") } + component.with_description { t(".description") } + end + %> + + <%= render(Admin::ScimClients::TokenTableComponent.new(rows: access_tokens)) %> + + <%= primer_form_with(url: admin_scim_client_static_tokens_path(model), method: :post) do %> + <%= render( + Primer::Beta::Button.new( + mt: 3, + type: :submit, + "aria-label": t(".label_aria_add_token"), + test_selector: "op-scim-clients--add-token-button" + ) + ) do |button| + button.with_leading_visual_icon(icon: :plus) + t(".label_add_token") + end %> + <% end %> +<% end %> diff --git a/app/components/admin/scim_clients/token_list_component.rb b/app/components/admin/scim_clients/token_list_component.rb new file mode 100644 index 00000000000..98b465fcd69 --- /dev/null +++ b/app/components/admin/scim_clients/token_list_component.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. +#++ + +module Admin + module ScimClients + class TokenListComponent < ApplicationComponent + include OpTurbo::Streamable + + def self.wrapper_key = :scim_clients_token_list + + def access_tokens + model.access_tokens.order(created_at: :desc) + end + end + end +end diff --git a/app/components/admin/scim_clients/token_row_component.rb b/app/components/admin/scim_clients/token_row_component.rb new file mode 100644 index 00000000000..dc9ec24db3b --- /dev/null +++ b/app/components/admin/scim_clients/token_row_component.rb @@ -0,0 +1,78 @@ +# 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 Admin::ScimClients + class TokenRowComponent < OpPrimer::BorderBoxRowComponent + def created_at + format_date(model.created_at) + end + + def expires_at + if model.revoked? + t("admin.scim_clients.token_table_component.revoked", date: format_date(model.revoked_at)) + elsif model.expired? + t("admin.scim_clients.token_table_component.expired", date: format_date(model.expires_at)) + else + format_date(model.expires_at) + end + end + + def button_links + [revoke_button] # invisible button outside of menu + end + + def revoke_button + return if model.revoked? || model.expired? + + render( + Primer::Beta::IconButton.new( + scheme: :invisible, + "aria-label": t("button_revoke"), + icon: :"no-entry", + tag: :a, + href: deletion_dialog_admin_scim_client_static_token_path(model, scim_client_id: scim_client.id, + target: TokenListComponent.wrapper_key), + data: { controller: "async-dialog" }, + test_selector: "op-scim-clients--revoke-token-button" + ) + ) + end + + private + + def scim_client + model.application.integration + end + + def format_date(date) + helpers.format_date(date) + end + end +end diff --git a/app/components/admin/scim_clients/token_table_component.rb b/app/components/admin/scim_clients/token_table_component.rb new file mode 100644 index 00000000000..b3c0484ad38 --- /dev/null +++ b/app/components/admin/scim_clients/token_table_component.rb @@ -0,0 +1,63 @@ +# 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 Admin::ScimClients + class TokenTableComponent < OpPrimer::BorderBoxTableComponent + columns :created_at, :expires_at + mobile_labels :created_at, :expires_at + + def mobile_title + t(".title") + end + + def row_class + TokenRowComponent + end + + def headers + [ + [:created_at, { caption: Doorkeeper::AccessToken.human_attribute_name(:created_at) }], + [:expires_at, { caption: Doorkeeper::AccessToken.human_attribute_name(:expires_at) }] + ] + end + + def blank_title + t(".blank_slate.title") + end + + def blank_description + t(".blank_slate.description") + end + + def has_actions? + true + end + end +end diff --git a/app/contracts/scim_clients/create_contract.rb b/app/contracts/scim_clients/create_contract.rb index 14eb9028671..dc2ab73e906 100644 --- a/app/contracts/scim_clients/create_contract.rb +++ b/app/contracts/scim_clients/create_contract.rb @@ -29,6 +29,17 @@ #++ module ScimClients - class CreateContract < BaseContract + class CreateContract < ModelContract + attribute :name + validates :name, presence: true + + attribute :auth_provider + validates :auth_provider, presence: true + + attribute :authentication_method + validates :authentication_method, inclusion: { in: ScimClient.authentication_methods.keys } + + attribute :jwt_sub + validates :jwt_sub, presence: true, if: -> { @model.authentication_method_sso? } end end diff --git a/app/contracts/scim_clients/update_contract.rb b/app/contracts/scim_clients/update_contract.rb index e870651ebde..4400d5283dd 100644 --- a/app/contracts/scim_clients/update_contract.rb +++ b/app/contracts/scim_clients/update_contract.rb @@ -29,6 +29,7 @@ #++ module ScimClients - class UpdateContract < BaseContract + class UpdateContract < CreateContract + attribute :authentication_method, writable: false end end diff --git a/app/controllers/admin/scim_client_static_tokens_controller.rb b/app/controllers/admin/scim_client_static_tokens_controller.rb new file mode 100644 index 00000000000..c7ed5eb512a --- /dev/null +++ b/app/controllers/admin/scim_client_static_tokens_controller.rb @@ -0,0 +1,63 @@ +# 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 Admin + class ScimClientStaticTokensController < ::ApplicationController + include OpTurbo::ComponentStream + + before_action :require_admin + + def create + scim_client = ScimClient.find(params[:scim_client_id]) + result = ::ScimClients::GenerateStaticTokenService.new(scim_client).call + + update_via_turbo_stream(component: Admin::ScimClients::TokenListComponent.new(scim_client)) + + respond_with_dialog ScimClients::CreatedTokenDialogComponent.new(result.result) + end + + def deletion_dialog + respond_with_dialog ScimClients::RevokeStaticTokenDialogComponent.new( + Doorkeeper::AccessToken.find(params[:id]), + scim_client_id: params[:scim_client_id], + turbo_frame: params[:target].presence + ) + end + + def destroy + token = Doorkeeper::AccessToken.find(params[:id]) + scim_client = ScimClient.find(params[:scim_client_id]) + + ::ScimClients::RevokeStaticTokenService.new(scim_client).call(token) + + redirect_to edit_admin_scim_client_path(scim_client) + end + end +end diff --git a/app/controllers/admin/scim_clients_controller.rb b/app/controllers/admin/scim_clients_controller.rb new file mode 100644 index 00000000000..1611d00db2b --- /dev/null +++ b/app/controllers/admin/scim_clients_controller.rb @@ -0,0 +1,132 @@ +# 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 Admin + class ScimClientsController < ::ApplicationController + include OpTurbo::ComponentStream + + before_action :require_admin + + menu_item :scim_clients + + layout "admin" + + def index + @scim_clients = ScimClient.order(:name) + end + + def new + @scim_client = ScimClient.new(authentication_method: :oauth2_token) + end + + def edit + @scim_client = ScimClient.find(params[:id]) + + first_time_setup(@scim_client) + end + + def create + result = ::ScimClients::CreateService.new(user: User.current).call(scim_client_params) + result.on_failure do + @scim_client = result.result + stream_form_component do |format| + format.html { render :new } + end + end + + result.on_success do + flash[:notice] = t(:notice_successful_create) + redirect_to edit_admin_scim_client_path(result.result, first_time_setup: true) + end + end + + def update + @scim_client = ScimClient.find(params[:id]) + result = ::ScimClients::UpdateService.new(user: User.current, model: @scim_client).call(scim_client_params) + + result.on_failure do + stream_form_component do |format| + format.html { render :edit } + end + end + + result.on_success do + flash[:notice] = t(:notice_successful_update) + redirect_to action: :index + end + end + + def deletion_dialog + respond_with_dialog ScimClients::DeleteScimClientDialogComponent.new(ScimClient.find(params[:id])) + end + + def destroy + model = ScimClient.find(params[:id]) + result = ::ScimClients::DeleteService.new(user: User.current, model:).call + + if result.success? + flash[:notice] = I18n.t(:notice_successful_delete) + else + flash[:error] = result.errors.full_messages + end + + redirect_to action: :index + end + + private + + def scim_client_params + params.expect(scim_client: %i[name auth_provider_id authentication_method jwt_sub]) + end + + def first_time_setup(scim_client) + return if params[:first_time_setup].blank? + + case scim_client.authentication_method + when "oauth2_token" + if scim_client.access_tokens.empty? + ::ScimClients::GenerateStaticTokenService.new(scim_client).call + @setup_token = true + end + when "oauth2_client" + # Ensuring that the client secret can't infinitely be accessed by calling with ?first_time_setup=true long after + # the initial setup (there is no other persisted marker showing us, that this is the first time) + if scim_client.oauth_application.created_at > 1.minute.ago + @setup_client_credentials = true + end + end + end + + def stream_form_component(&) + update_via_turbo_stream(component: Admin::ScimClients::FormComponent.new(@scim_client)) + respond_with_turbo_streams(&) + end + end +end diff --git a/app/forms/scim_clients/form.rb b/app/forms/scim_clients/form.rb new file mode 100644 index 00000000000..f6fb73e15da --- /dev/null +++ b/app/forms/scim_clients/form.rb @@ -0,0 +1,105 @@ +# 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 ScimClients + class Form < ApplicationForm + form do |client_form| + client_form.text_field( + name: :name, + label: ScimClient.human_attribute_name(:name), + required: true, + caption: I18n.t("admin.scim_clients.form.name_description"), + input_width: :large + ) + + client_form.select_list( + name: :auth_provider_id, + label: ScimClient.human_attribute_name(:auth_provider_id), + caption: I18n.t("admin.scim_clients.form.auth_provider_description"), + input_width: :large, + include_blank: false + ) do |select| + AuthProvider.find_each do |provider| + select.option( + value: provider.id, + label: provider.display_name + ) + end + end + + client_form.select_list( + name: :authentication_method, + label: ScimClient.human_attribute_name(:authentication_method), + caption: I18n.t("admin.scim_clients.form.authentication_method_description_html").html_safe, + input_width: :large, + include_blank: false, + disabled: model.persisted?, + data: { + action: "scim-clients--form-inputs#updateFormInputs", + "scim-clients--form-inputs-target": "authenticationMethodInput" + } + ) do |select| + ScimClient.authentication_methods.each_key do |method| + select.option( + value: method, + label: I18n.t("admin.scim_clients.authentication_methods.#{method}") + ) + end + end + + client_form.group(data: { "scim-clients--form-inputs-target": "jwtSubInputWrapper" }) do |group| + group.text_field( + name: :jwt_sub, + label: ScimClient.human_attribute_name(:jwt_sub), + required: true, + caption: I18n.t("admin.scim_clients.form.jwt_sub_description_html", docs_url: "#").html_safe, # TODO: correct docs url + input_width: :large + ) + end + + if show_client_id? + client_form.html_content do + render(Admin::ScimClients::ClientIdComponent.new(model)) + end + end + + client_form.submit( + name: :submit, + label: model.persisted? ? I18n.t(:button_save) : I18n.t(:button_create), + scheme: :primary, + data: { "scim-clients--form-inputs-target": "submitButton" } + ) + end + + def show_client_id? + model.persisted? && model.authentication_method_oauth2_client? + end + end +end diff --git a/app/models/scim_client.rb b/app/models/scim_client.rb index 079d3140730..22828768010 100644 --- a/app/models/scim_client.rb +++ b/app/models/scim_client.rb @@ -41,4 +41,23 @@ class ScimClient < ApplicationRecord oauth2_client: 1, oauth2_token: 2 }, scopes: false, prefix: true + + def access_tokens + return Doorkeeper::AccessToken.none unless authentication_method_oauth2_token? + + oauth_application.access_tokens + end + + def jwt_sub + auth_provider_link&.external_id + end + + # This method is part of a nasty workaround for creating and updating SCIM clients: + # To be able to validate the jwt_sub, the SetAttributesService must be able to effectively set the jwt_sub, + # before it's validated by a contract. Afterwards the UpdateService must be able to persist the change. Since + # user_auth_provider_links is a has_many association, there is no built-in memoization for values. So to make sure the + # SetAttributesService, the Contract and the UpdateService all look at the same jwt_sub, we memoize the auth_provider_link here + def auth_provider_link + @auth_provider_link ||= service_account&.user_auth_provider_links&.first + end end diff --git a/app/services/scim_clients/create_service.rb b/app/services/scim_clients/create_service.rb index bd113770474..4201235683f 100644 --- a/app/services/scim_clients/create_service.rb +++ b/app/services/scim_clients/create_service.rb @@ -33,24 +33,12 @@ class ScimClients::CreateService < BaseServices::Create super.tap do |service_result| self.model = service_result.result - update_service_account update_oauth_application(service_result) end end private - def update_service_account - service_account.name = params[:name] - if model.authentication_method_sso? - service_account.user_auth_provider_links.build( - auth_provider_id: params[:auth_provider_id], - external_id: params[:jwt_sub] - ) - end - service_account.save! - end - def update_oauth_application(service_result) return if !model.authentication_method_oauth2_client? && !model.authentication_method_oauth2_token? @@ -60,7 +48,7 @@ class ScimClients::CreateService < BaseServices::Create end def service_account - @service_account ||= model.build_service_account(admin: true) + model.service_account end def create_oauth_application @@ -69,6 +57,7 @@ class ScimClients::CreateService < BaseServices::Create .call( name: "#{model.name} (#{ScimClient.model_name.human})", redirect_uri: "urn:ietf:wg:oauth:2.0:oob", + client_credentials_user_id: service_account.id, scopes: "scim_v2", confidential: true, integration: model, diff --git a/app/services/scim_clients/delete_service.rb b/app/services/scim_clients/delete_service.rb index 42b100da4ea..9289a0da40e 100644 --- a/app/services/scim_clients/delete_service.rb +++ b/app/services/scim_clients/delete_service.rb @@ -29,7 +29,7 @@ #++ class ScimClients::DeleteService < BaseServices::Delete - def before_perform(_, call) + def before_perform(call) # pre-loading service_account association before destroy to ensure it's available afterwards call.result.service_account call diff --git a/app/services/scim_clients/generate_static_token_service.rb b/app/services/scim_clients/generate_static_token_service.rb new file mode 100644 index 00000000000..9531df753fa --- /dev/null +++ b/app/services/scim_clients/generate_static_token_service.rb @@ -0,0 +1,54 @@ +# 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. +#++ + +class ScimClients::GenerateStaticTokenService < BaseServices::BaseCallable + def initialize(scim_client) + super() + + @scim_client = scim_client + end + + def perform + return ServiceResult.failure unless @scim_client.authentication_method_oauth2_token? + + token = @scim_client.oauth_application.access_tokens.create(scopes: "scim_v2", expires_in:) + if token.persisted? + ServiceResult.success(result: token) + else + ServiceResult.failure(errors: token.errors) + end + end + + private + + def expires_in + (1.year.from_now - Time.zone.now).to_i + end +end diff --git a/app/services/scim_clients/revoke_static_token_service.rb b/app/services/scim_clients/revoke_static_token_service.rb new file mode 100644 index 00000000000..b3b04d93719 --- /dev/null +++ b/app/services/scim_clients/revoke_static_token_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class ScimClients::RevokeStaticTokenService < BaseServices::BaseCallable + def initialize(scim_client) + super() + + @scim_client = scim_client + end + + def perform(access_token) + return ServiceResult.failure if access_token.application.integration != @scim_client + + access_token.revoke unless access_token.revoked? + + ServiceResult.success(result: access_token) + end +end diff --git a/app/services/scim_clients/set_attributes_service.rb b/app/services/scim_clients/set_attributes_service.rb index 8c7c0980128..f41109eb1c4 100644 --- a/app/services/scim_clients/set_attributes_service.rb +++ b/app/services/scim_clients/set_attributes_service.rb @@ -34,6 +34,24 @@ module ScimClients def set_attributes(params) super(params.except(:jwt_sub)) + update_service_account + end + + def update_service_account + service_account.assign_attributes(params.slice(:name)) + + if model.authentication_method_sso? + auth_provider_link.assign_attributes(params.slice(:auth_provider_id)) + auth_provider_link.external_id = params[:jwt_sub] if params.key?(:jwt_sub) + end + end + + def service_account + model.service_account || model.build_service_account(admin: true) + end + + def auth_provider_link + @auth_provider_link ||= model.auth_provider_link || service_account.user_auth_provider_links.build end end end diff --git a/app/services/scim_clients/update_service.rb b/app/services/scim_clients/update_service.rb index 656cfcdc260..b2fc0cfc5f5 100644 --- a/app/services/scim_clients/update_service.rb +++ b/app/services/scim_clients/update_service.rb @@ -31,26 +31,9 @@ class ScimClients::UpdateService < BaseServices::Update def after_perform(_) super.tap do |result| - update_service_account(result.result) + scim_client = result.result + scim_client.service_account&.save! + scim_client.auth_provider_link&.save! end end - - private - - def update_service_account(scim_client) - scim_client.service_account&.update!(params.slice(:name)) - - if model.authentication_method_sso? - link = scim_client.service_account&.user_auth_provider_links&.find_or_initialize_by({}) - update_user_auth_provider_link(link) - end - end - - def update_user_auth_provider_link(link) - return if link.nil? - - link.auth_provider_id = params[:auth_provider_id] if params.key?(:auth_provider_id) - link.external_id = params[:jwt_sub] if params.key?(:jwt_sub) - link.save! - end end diff --git a/app/views/admin/scim_clients/edit.html.erb b/app/views/admin/scim_clients/edit.html.erb new file mode 100644 index 00000000000..243c2de6787 --- /dev/null +++ b/app/views/admin/scim_clients/edit.html.erb @@ -0,0 +1,66 @@ +<%#-- 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. + +++#%> + +<% html_title t(:label_administration), ScimClient.model_name.human(count: 2), @scim_client.name %> + +<%= + render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title { @scim_client.name } + header.with_description { t("admin.scim_clients.form.description_html", docs_url: "#") } # TODO: Deep link into documentation + header.with_breadcrumbs( + [{ href: admin_index_path, text: t(:label_administration) }, + { href: admin_settings_authentication_path, text: t(:label_authentication) }, + { href: admin_scim_clients_path, text: ScimClient.model_name.human(count: 2) }, + @scim_client.name] + ) + + header.with_action_button( + scheme: :danger, + mobile_icon: :trash, + mobile_label: t("button_delete"), + aria: { label: t(".label_delete_scim_client") }, + tag: :a, + href: deletion_dialog_admin_scim_client_path(@scim_client), + data: { controller: "async-dialog" } + ) do |button| + button.with_leading_visual_icon(icon: :trash) + t("button_delete") + end + end +%> + +<%= render(Admin::ScimClients::FormComponent.new(@scim_client)) %> + +<%= render(Admin::ScimClients::TokenListComponent.new(@scim_client)) if @scim_client.authentication_method_oauth2_token? %> + +<% if @setup_token %> + <%= render(Admin::ScimClients::CreatedTokenDialogComponent.new(@scim_client.access_tokens.first, data: { controller: "auto-show-dialog" })) %> +<% elsif @setup_client_credentials %> + <%= render(Admin::ScimClients::CreatedClientCredentialsDialogComponent.new(@scim_client, data: { controller: "auto-show-dialog" })) %> +<% end %> diff --git a/app/views/admin/scim_clients/index.html.erb b/app/views/admin/scim_clients/index.html.erb new file mode 100644 index 00000000000..e294e1766f2 --- /dev/null +++ b/app/views/admin/scim_clients/index.html.erb @@ -0,0 +1,55 @@ +<%#-- 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. + +++#%> + +<% html_title t(:label_administration), ScimClient.model_name.human(count: 2) %> + +<%= + render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title { ScimClient.model_name.human(count: 2) } + header.with_description { t(".description") } + header.with_breadcrumbs( + [{ href: admin_index_path, text: t(:label_administration) }, + { href: admin_settings_authentication_path, text: t(:label_authentication) }, + ScimClient.model_name.human(count: 2)] + ) + end +%> + +<%= + render(Primer::OpenProject::SubHeader.new) do |component| + component.with_action_button(leading_icon: :plus, + label: t(".label_create_button"), + scheme: :primary, + tag: :a, + href: new_admin_scim_client_path) { ScimClient.model_name.human } + end +%> + + +<%= render(Admin::ScimClients::TableComponent.new(rows: @scim_clients)) %> diff --git a/app/views/admin/scim_clients/new.html.erb b/app/views/admin/scim_clients/new.html.erb new file mode 100644 index 00000000000..7f7a2986c6e --- /dev/null +++ b/app/views/admin/scim_clients/new.html.erb @@ -0,0 +1,45 @@ +<%#-- 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. + +++#%> + +<% html_title t(:label_administration), ScimClient.model_name.human(count: 2), t(".title") %> + +<%= + render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title { t(".title") } + header.with_description { t("admin.scim_clients.form.description_html", docs_url: "#") } # TODO: Deep link into documentation + header.with_breadcrumbs( + [{ href: admin_index_path, text: t(:label_administration) }, + { href: admin_settings_authentication_path, text: t(:label_authentication) }, + { href: admin_scim_clients_path, text: ScimClient.model_name.human(count: 2) }, + t(".title")] + ) + end +%> + +<%= render(Admin::ScimClients::FormComponent.new(@scim_client)) %> diff --git a/config/initializers/feature_decisions.rb b/config/initializers/feature_decisions.rb index bfb974e9593..a8569624074 100644 --- a/config/initializers/feature_decisions.rb +++ b/config/initializers/feature_decisions.rb @@ -49,3 +49,6 @@ OpenProject::FeatureDecisions.add :calculated_value_project_attribute, OpenProject::FeatureDecisions.add :block_note_editor, description: "Enables the block note editor for rich text fields where available." + +OpenProject::FeatureDecisions.add :scim_api, + description: "Enables SCIM API." diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index 364a444ff32..e075b44873b 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -510,14 +510,6 @@ Redmine::MenuManager.map :admin_menu do |menu| caption: :"authentication.login_and_registration", parent: :authentication - menu.push :ldap_authentication, - { controller: "/ldap_auth_sources", action: "index" }, - if: ->(_) { User.current.admin? && !OpenProject::Configuration.disable_password_login? }, - parent: :authentication, - caption: :label_ldap_auth_source_plural, - html: { class: "server_authentication" }, - last: true - menu.push :oauth_applications, { controller: "/oauth/applications", action: "index" }, if: ->(_) { User.current.admin? }, @@ -525,6 +517,19 @@ Redmine::MenuManager.map :admin_menu do |menu| caption: :"oauth.application.plural", html: { class: "oauth_applications" } + menu.push :ldap_authentication, + { controller: "/ldap_auth_sources", action: "index" }, + if: ->(_) { User.current.admin? && !OpenProject::Configuration.disable_password_login? }, + parent: :authentication, + caption: :label_ldap_auth_source_plural, + html: { class: "server_authentication" } + + menu.push :scim_clients, + { controller: "/admin/scim_clients", action: "index" }, + if: ->(_) { User.current.admin? && OpenProject::FeatureDecisions.scim_api_active? }, + parent: :authentication, + caption: ScimClient.model_name.human(count: 2) + menu.push :announcements, { controller: "/announcements", action: "edit" }, if: ->(_) { User.current.admin? }, diff --git a/config/locales/en.yml b/config/locales/en.yml index d99dc7e7c6a..6e3c717a965 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -119,6 +119,59 @@ en: explanation: text: "Individual actions of a user (e.g. updating a work package twice) are aggregated into a single action if their age difference is less than the specified timespan. They will be displayed as a single action within the application. This will also delay notifications by the same amount of time reducing the number of emails being sent and will also affect %{webhook_link} delay." link: "webhook" + scim_clients: + authentication_methods: + sso: "JWT from identity provider" + oauth2_client: "OAuth 2.0 client credentials" + oauth2_token: "Static access token" + created_client_credentials_dialog_component: + title: "Client credentials created" + heading: "Client credentials have been generated" + one_time_hint: "This is the only time you will see the client secret. Make sure to copy it now." + created_token_dialog_component: + title: "Token created" + heading: "An token has been generated" + label_token: "Token" + one_time_hint: "This is the only time you will see this token. Make sure to copy it now." + delete_scim_client_dialog_component: + title: "Delete SCIM client" + heading: "Are you sure you want to delete this SCIM client?" + description: "Users managed by this SCIM client can no longer be updated by it." + edit: + label_delete_scim_client: "Delete SCIM client" + form: + auth_provider_description: "This is the service that users added by the SCIM provider will use to authenticate in OpenProject." + authentication_method_description_html: "This is how the SCIM client authenticates at OpenProject. Please ensure that OAuth tokens include the scim_v2 scope." + description_html: 'Please refer to our documentation on configuring SCIM clients for more information on these configuration options.' + jwt_sub_description_html: 'For example, for Keycloak, this is the UUID of the service account associated with the SCIM client. Consult our documentation to learn how to find the subject claim for your use case.' + name_description: "Choose a name that will help other admins better understand why this client was configured." + index: + description: "SCIM clients are able to interact with OpenProject SCIM server API to provision, update, and deprovision user accounts and groups." + label_create_button: "Add SCIM client" + new: + title: "New SCIM client" + revoke_static_token_dialog_component: + confirm_button: "Revoke" + title: "Revoke static token" + heading: "Are you sure you want to revoke this token?" + description: "The SCIM client that uses this token will no longer be able to access OpenProject's SCIM server API." + table_component: + blank_slate: + title: "No SCIM clients configured yet" + description: "Add clients to see them here" + user_count: "Users" + token_list_component: + description: "The tokens you generate here can be passed by a SCIM client to access the OpenProject SCIM API." + heading: "Tokens" + label_add_token: "Token" + label_aria_add_token: "Add token" + token_table_component: + blank_slate: + title: "No tokens have been created yet" + description: "You can create one now" + expired: "Expired on %{date}" + revoked: "Revoked on %{date}" + title: "Access token table" authentication: login_and_registration: "Login and registration" @@ -966,7 +1019,7 @@ en: attribute_name: "Attribute" help_text: "Help text" auth_provider: - scim_clients: "SCIM Clients" + scim_clients: "SCIM clients" capability: context: "Context" changeset: @@ -1130,6 +1183,10 @@ en: url: "URL" role: permissions: "Permissions" + scim_client: + auth_provider: "Authentication provider" + authentication_method: "Authentication method" + jwt_sub: "Subject claim" status: is_closed: "Work package closed" is_readonly: "Work package read-only" @@ -1660,8 +1717,8 @@ en: one: "Role" other: "Roles" scim_client: - one: "SCIM Client" - other: "SCIM Clients" + one: "SCIM client" + other: "SCIM clients" status: "Work package status" token/api: one: Access token diff --git a/config/routes.rb b/config/routes.rb index 41a3cbaabe2..fa67b6a3dde 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -630,6 +630,18 @@ Rails.application.routes.draw do controller: "/admin/attachments/quarantined_attachments", only: %i[index destroy] + resources :scim_clients, only: %i[index edit new create update destroy] do + member do + get :deletion_dialog + end + + resources :static_tokens, only: %i[create destroy], controller: "/admin/scim_client_static_tokens" do + member do + get :deletion_dialog + end + end + end + resource :backups, controller: "/admin/backups", only: %i[show] do collection do get :reset_token diff --git a/frontend/src/stimulus/controllers/dynamic/scim-clients/form-inputs.controller.ts b/frontend/src/stimulus/controllers/dynamic/scim-clients/form-inputs.controller.ts new file mode 100644 index 00000000000..605e99d0a44 --- /dev/null +++ b/frontend/src/stimulus/controllers/dynamic/scim-clients/form-inputs.controller.ts @@ -0,0 +1,63 @@ +/* + * -- 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. + * ++ + */ + +import { Controller } from '@hotwired/stimulus'; + +export default class FormInputsController extends Controller { + static targets = [ + 'jwtSubInputWrapper', + 'authenticationMethodInput', + 'submitButton', + ]; + + declare readonly jwtSubInputWrapperTarget:HTMLDivElement; + declare readonly authenticationMethodInputTarget:HTMLInputElement; + declare readonly submitButtonTarget:HTMLButtonElement; + + connect() { + this.updateFormInputs(); + } + + updateFormInputs() { + if (this.authenticationMethodInputTarget.value === 'sso') { + this.showJwtSubInput(); + } else { + this.hideJwtSubInput(); + } + } + + showJwtSubInput() { + this.jwtSubInputWrapperTarget.classList.remove('d-none'); + } + + hideJwtSubInput() { + this.jwtSubInputWrapperTarget.classList.add('d-none'); + } +} diff --git a/modules/auth_saml/lib/open_project/auth_saml/engine.rb b/modules/auth_saml/lib/open_project/auth_saml/engine.rb index 9781f6059eb..8c0c7295891 100644 --- a/modules/auth_saml/lib/open_project/auth_saml/engine.rb +++ b/modules/auth_saml/lib/open_project/auth_saml/engine.rb @@ -27,6 +27,7 @@ module OpenProject :plugin_saml, :saml_providers_path, parent: :authentication, + after: :oauth_applications, caption: ->(*) { I18n.t("saml.menu_title") }, enterprise_feature: "sso_auth_providers" end diff --git a/modules/ldap_groups/lib/open_project/ldap_groups/engine.rb b/modules/ldap_groups/lib/open_project/ldap_groups/engine.rb index 0296a0bb454..55fe0fdb128 100644 --- a/modules/ldap_groups/lib/open_project/ldap_groups/engine.rb +++ b/modules/ldap_groups/lib/open_project/ldap_groups/engine.rb @@ -14,7 +14,7 @@ module OpenProject::LdapGroups :plugin_ldap_groups, { controller: "/ldap_groups/synchronized_groups", action: :index }, parent: :authentication, - last: true, + after: :ldap_authentication, caption: ->(*) { I18n.t("ldap_groups.label_menu_item") }, enterprise_feature: "ldap_groups" end diff --git a/modules/openid_connect/lib/open_project/openid_connect/engine.rb b/modules/openid_connect/lib/open_project/openid_connect/engine.rb index 02544027b16..a7d9b37a20f 100644 --- a/modules/openid_connect/lib/open_project/openid_connect/engine.rb +++ b/modules/openid_connect/lib/open_project/openid_connect/engine.rb @@ -45,6 +45,7 @@ module OpenProject::OpenIDConnect :plugin_openid_connect, :openid_connect_providers_path, parent: :authentication, + after: :oauth_applications, caption: ->(*) { I18n.t("openid_connect.menu_title") }, enterprise_feature: "sso_auth_providers" end diff --git a/spec/factories/scim_client_factory.rb b/spec/factories/scim_client_factory.rb index ef4deec36f8..b11e459432d 100644 --- a/spec/factories/scim_client_factory.rb +++ b/spec/factories/scim_client_factory.rb @@ -33,5 +33,15 @@ FactoryBot.define do sequence(:name) { |n| "SCIM Client #{n}" } auth_provider factory: :oidc_provider authentication_method { :sso } + + trait :oauth2_token do + authentication_method { :oauth2_token } + oauth_application + end + + trait :oauth2_client do + authentication_method { :oauth2_client } + oauth_application + end end end diff --git a/spec/features/admin/scim_clients/create_spec.rb b/spec/features/admin/scim_clients/create_spec.rb new file mode 100644 index 00000000000..234b28d58d2 --- /dev/null +++ b/spec/features/admin/scim_clients/create_spec.rb @@ -0,0 +1,103 @@ +# 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" + +RSpec.describe "Creating a SCIM client", :js, :selenium, driver: :firefox_de do + shared_let(:admin) { create(:admin, preferences: { time_zone: "Etc/UTC" }) } + shared_let(:oidc_provider) { create(:oidc_provider) } + + current_user { admin } + + it "can create a SCIM client authenticating through JWT", :aggregate_failures do + visit new_admin_scim_client_path + + expect(page).to be_axe_clean.within("#content") + .skipping("link-in-text-block") # https://community.openproject.org/wp/65252 + + expect(page).to have_no_field("Subject claim") + select "JWT from identity provider", from: "Authentication method" + expect(page).to have_field("Subject claim") + + select oidc_provider.display_name, from: "Authentication provider" + + click_on("Create") # forgot to fill out form + expect(page).to have_text("Name can't be blank") + expect(page).to have_text("Subject claim can't be blank") + + fill_in "Name", with: "My SCIM Client" + fill_in "Subject claim", with: "123-abc-456-def" + click_on("Create") + wait_for { ScimClient.find_by(name: "My SCIM Client") }.not_to be_nil + + created_client = ScimClient.find_by(name: "My SCIM Client") + expect(page).to have_current_path(edit_admin_scim_client_path(created_client, first_time_setup: true)) + expect(created_client.auth_provider_id).to eq(oidc_provider.id) + expect(created_client.authentication_method).to eq("sso") + expect(created_client.auth_provider_link&.external_id).to eq("123-abc-456-def") + end + + it "can create a SCIM client authenticating through client credentials" do + visit new_admin_scim_client_path + + fill_in "Name", with: "My SCIM Client" + select oidc_provider.display_name, from: "Authentication provider" + select "OAuth 2.0 client credentials", from: "Authentication method" + + click_on("Create") + wait_for { ScimClient.find_by(name: "My SCIM Client") }.not_to be_nil + + created_client = ScimClient.find_by(name: "My SCIM Client") + expect(page).to have_current_path(edit_admin_scim_client_path(created_client, first_time_setup: true)) + + page.within_modal("Client credentials created") do + expect(page).to have_field("Client ID", with: created_client.oauth_application.uid) + expect(page).to have_field("Client secret", with: created_client.oauth_application.secret) + end + end + + it "can create a SCIM client authenticating through a static access token" do + visit new_admin_scim_client_path + + fill_in "Name", with: "My SCIM Client" + select oidc_provider.display_name, from: "Authentication provider" + select "Static access token", from: "Authentication method" + + click_on("Create") + wait_for { ScimClient.find_by(name: "My SCIM Client") }.not_to be_nil + + created_client = ScimClient.find_by(name: "My SCIM Client") + expect(page).to have_current_path(edit_admin_scim_client_path(created_client, first_time_setup: true)) + + page.within_modal("Token created") do + expect(page).to have_field("Token", with: created_client.oauth_application.access_tokens.last.token) + end + end +end diff --git a/spec/features/admin/scim_clients/index_spec.rb b/spec/features/admin/scim_clients/index_spec.rb new file mode 100644 index 00000000000..a0382ea4259 --- /dev/null +++ b/spec/features/admin/scim_clients/index_spec.rb @@ -0,0 +1,75 @@ +# 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" + +RSpec.describe "Listing SCIM clients", :js, :selenium, driver: :firefox_de do + shared_let(:admin) { create(:admin, preferences: { time_zone: "Etc/UTC" }) } + + current_user { admin } + + context "when there are no SCIM clients" do + it "renders a proper blank slate" do + visit admin_scim_clients_path + + expect(page).to be_axe_clean.within "#content" + + within_test_selector("Admin::ScimClients::TableComponent") do + expect(page).to have_content("No SCIM clients configured yet") + expect(page).to have_content("Add clients to see them here") + end + + within(".SubHeader") do + click_on "SCIM client" + end + + expect(page).to have_current_path(new_admin_scim_client_path) + end + end + + context "when there are SCIM clients" do + let!(:sso_client) { create(:scim_client) } + + it "renders a proper clients table" do + visit admin_scim_clients_path + + expect(page).to be_axe_clean.within "#content" + + within_test_selector("Admin::ScimClients::TableComponent") do + within(".name") { expect(page).to have_content(sso_client.name) } + within(".authentication_method") { expect(page).to have_content("JWT from identity provider") } + + click_on(sso_client.name) + end + + expect(page).to have_current_path(edit_admin_scim_client_path(sso_client)) + end + end +end diff --git a/spec/features/admin/scim_clients/update_spec.rb b/spec/features/admin/scim_clients/update_spec.rb new file mode 100644 index 00000000000..83b035f6f9b --- /dev/null +++ b/spec/features/admin/scim_clients/update_spec.rb @@ -0,0 +1,166 @@ +# 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" + +RSpec.describe "Updating a SCIM client", :js, :selenium, driver: :firefox_de do + shared_let(:admin) { create(:admin, preferences: { time_zone: "Etc/UTC" }) } + shared_let(:auth_provider) { create(:oidc_provider) } + + let(:sso_scim_client) { create(:scim_client, auth_provider:) } + let(:oauth_client_scim_client) { create(:scim_client, :oauth2_client, auth_provider:) } + let(:token_scim_client) { create(:scim_client, :oauth2_token, auth_provider:) } + + let!(:static_tokens) do + application = token_scim_client.oauth_application + [ + create(:oauth_access_token, application:, created_at: 2.weeks.ago, expires_in: 2.days.to_i), + create(:oauth_access_token, application:, created_at: 1.week.ago, expires_in: 2.weeks.to_i) + ] + end + + current_user { admin } + + it "can update a SCIM client authenticating through JWT", :aggregate_failures do + visit edit_admin_scim_client_path(sso_scim_client) + expect(page).to be_axe_clean.within("#content") + .skipping("link-in-text-block") # https://community.openproject.org/wp/65252 + + expect(page.find_field("Authentication method", disabled: :all)).to be_disabled + fill_in "Name", with: "" + fill_in "Subject claim", with: "" + click_on "Save" + + expect(page).to have_text("Name can't be blank") + expect(page).to have_text("Subject claim can't be blank") + + fill_in "Name", with: "New SSO name" + fill_in "Subject claim", with: "new-claim" + click_on "Save" + + expect(page).to have_current_path(admin_scim_clients_path) + within_test_selector("Admin::ScimClients::TableComponent") do + expect(page).to have_text("New SSO name") + end + + visit edit_admin_scim_client_path(sso_scim_client) + + within(".PageHeader") { click_on "Delete" } + page.within_modal("Delete SCIM client") { click_on "Delete" } + expect(page).to have_current_path(admin_scim_clients_path) + expect(ScimClient.where(id: sso_scim_client.id)).to be_empty + end + + it "can update a SCIM client authenticating through client credentials", :aggregate_failures do + visit edit_admin_scim_client_path(oauth_client_scim_client) + expect(page).to be_axe_clean.within("#content") + .skipping("link-in-text-block") # https://community.openproject.org/wp/65252 + + expect(page.find_field("Authentication method", disabled: :all)).to be_disabled + fill_in "Name", with: "" + click_on "Save" + + expect(page).to have_text("Name can't be blank") + + fill_in "Name", with: "New client credentials name" + click_on "Save" + + expect(page).to have_current_path(admin_scim_clients_path) + within_test_selector("Admin::ScimClients::TableComponent") do + expect(page).to have_text("New client credentials name") + end + + visit edit_admin_scim_client_path(oauth_client_scim_client) + + expect(page).to have_field("Client ID", with: oauth_client_scim_client.oauth_application.uid) + expect(page).to have_no_field("Client secret") + + within(".PageHeader") { click_on "Delete" } + page.within_modal("Delete SCIM client") { click_on "Delete" } + expect(page).to have_current_path(admin_scim_clients_path) + expect(ScimClient.where(id: oauth_client_scim_client.id)).to be_empty + end + + it "can update a SCIM client authenticating through a static access token", :aggregate_failures do + visit edit_admin_scim_client_path(token_scim_client) + expect(page).to be_axe_clean.within("#content") + .skipping("link-in-text-block") # https://community.openproject.org/wp/65252 + + expect(page.find_field("Authentication method", disabled: :all)).to be_disabled + fill_in "Name", with: "" + click_on "Save" + + expect(page).to have_text("Name can't be blank") + + fill_in "Name", with: "New static token name" + click_on "Save" + + expect(page).to have_current_path(admin_scim_clients_path) + within_test_selector("Admin::ScimClients::TableComponent") do + expect(page).to have_text("New static token name") + end + + visit edit_admin_scim_client_path(token_scim_client) + + within_test_selector("Admin::ScimClients::TokenTableComponent") do + expect(page).to have_css(".created_at").twice + expect(page).to have_css(".expires_at").twice + expect(page).to have_text("Expired on").once + expect(page).to have_no_text("Revoked on") + + expect(page).to have_test_selector("op-scim-clients--revoke-token-button").once + page.find_test_selector("op-scim-clients--revoke-token-button").click + end + + page.within_modal("Revoke static token") { click_on "Revoke" } + + within_test_selector("Admin::ScimClients::TokenTableComponent") do + expect(page).to have_text("Revoked on").once + expect(page).to have_no_test_selector("op-scim-clients--revoke-token-button") + end + + page.find_test_selector("op-scim-clients--add-token-button").click + within_modal("Token created") do + expect(page).to have_field("Token", with: Doorkeeper::AccessToken.last.token) + click_on("Close") + end + within_test_selector("Admin::ScimClients::TokenTableComponent") do + expect(page).to have_css(".created_at").exactly(3).times + expect(page).to have_css(".expires_at").exactly(3).times + + expect(page).to have_test_selector("op-scim-clients--revoke-token-button").once + end + + within(".PageHeader") { click_on "Delete" } + page.within_modal("Delete SCIM client") { click_on "Delete" } + expect(page).to have_current_path(admin_scim_clients_path) + expect(ScimClient.where(id: token_scim_client.id)).to be_empty + end +end diff --git a/spec/models/scim_clients/form_model_spec.rb b/spec/models/scim_clients/form_model_spec.rb deleted file mode 100644 index 1cdabea6a22..00000000000 --- a/spec/models/scim_clients/form_model_spec.rb +++ /dev/null @@ -1,82 +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. -#++ - -require "spec_helper" - -RSpec.describe ScimClients::FormModel do - describe ".from_client" do - subject { described_class.from_client(client) } - - let(:client) do - create( - :scim_client, service_account: create(:service_account, authentication_provider: auth_provider, external_id: "abc-def"), - authentication_method: :sso, - auth_provider: - ) - end - let(:auth_provider) { create(:oidc_provider) } - - it "builds a proper FormModel", :aggregate_failures do - expect(subject.name).to eq(client.name) - expect(subject.auth_provider_id).to eq(auth_provider.id) - expect(subject.authentication_method).to eq("sso") - expect(subject.jwt_sub).to eq("abc-def") - end - - context "when the auth provider link is missing" do - let(:client) do - create :scim_client, service_account: create(:service_account), - authentication_method: :sso, - auth_provider: - end - - it "fills the auth_provider_id correctly" do - expect(subject.auth_provider_id).to eq(auth_provider.id) - end - - it "leaves the jwt_sub blank" do - expect(subject.jwt_sub).to be_nil - end - end - end - - describe ".from_params" do - subject { described_class.from_params(params) } - - let(:params) { { name: "The Client", auth_provider_id: 42, authentication_method: :banana, jwt_sub: "a-sub" } } - - it "builds a proper FormModel", :aggregate_failures do - expect(subject.name).to eq("The Client") - expect(subject.auth_provider_id).to eq(42) - expect(subject.authentication_method).to eq("banana") - expect(subject.jwt_sub).to eq("a-sub") - end - end -end diff --git a/spec/services/scim_clients/generate_static_token_service_spec.rb b/spec/services/scim_clients/generate_static_token_service_spec.rb new file mode 100644 index 00000000000..79434a94ee0 --- /dev/null +++ b/spec/services/scim_clients/generate_static_token_service_spec.rb @@ -0,0 +1,68 @@ +# 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" + +RSpec.describe ScimClients::GenerateStaticTokenService do + subject(:service_result) { described_class.new(scim_client).call } + + let(:scim_client) { create(:scim_client, :oauth2_token) } + + it "returns a valid token", :aggregate_failures, :freeze_time do + expect(service_result).to be_success + + expect(service_result.result.expires_at).to eq(1.year.from_now) + expect(service_result.result.includes_scope?("scim_v2")).to be_truthy # rubocop:disable RSpec/PredicateMatcher + end + + it "generates a token" do + expect { subject }.to change(Doorkeeper::AccessToken, :count).by(1) + end + + context "when the SCIM client is authenticating through client credentials" do + let(:scim_client) { create(:scim_client, :oauth2_client) } + + it { is_expected.to be_failure } + + it "does not generate a token" do + expect { subject }.not_to change(Doorkeeper::AccessToken, :count) + end + end + + context "when the SCIM client is authenticating through IDP tokens" do + let(:scim_client) { create(:scim_client) } + + it { is_expected.to be_failure } + + it "does not generate a token" do + expect { subject }.not_to change(Doorkeeper::AccessToken, :count) + end + end +end diff --git a/spec/services/scim_clients/revoke_static_token_service_spec.rb b/spec/services/scim_clients/revoke_static_token_service_spec.rb new file mode 100644 index 00000000000..b953b504ef9 --- /dev/null +++ b/spec/services/scim_clients/revoke_static_token_service_spec.rb @@ -0,0 +1,79 @@ +# 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" + +RSpec.describe ScimClients::RevokeStaticTokenService do + subject(:service_result) { described_class.new(scim_client).call(token) } + + let(:scim_client) { create(:scim_client, :oauth2_token) } + let(:token) { scim_client.oauth_application.access_tokens.create!(expires_in: 60) } + + it "revokes the token effective immediately", :aggregate_failures, :freeze_time do + subject + expect(token.reload).to be_revoked + expect(token.revoked_at).to be_within(0.1).of(Time.zone.now) + end + + it { is_expected.to be_success } + + context "when the token is already revoked", :freeze_time do + let(:token) { scim_client.oauth_application.access_tokens.create!(expires_in: 60, revoked_at: 1.minute.ago) } + + it { is_expected.to be_success } + + it "doesn't set a new revoked_at" do + subject + expect(token.reload.revoked_at).to be_within(0.1).of(1.minute.ago) + end + end + + context "when the token belongs to a different SCIM client" do + let(:token) { create(:scim_client, :oauth2_token).oauth_application.access_tokens.create!(expires_in: 60) } + + it { is_expected.to be_failure } + + it "does not revoke the token" do + subject + expect(token.reload).not_to be_revoked + end + end + + context "when the token belongs to no SCIM client at all" do + let(:token) { create(:oauth_application).access_tokens.create!(expires_in: 60) } + + it { is_expected.to be_failure } + + it "does not revoke the token" do + subject + expect(token.reload).not_to be_revoked + end + end +end diff --git a/spec/support/finders/test_selector_finders.rb b/spec/support/finders/test_selector_finders.rb index 99a813792a4..afd9184badb 100644 --- a/spec/support/finders/test_selector_finders.rb +++ b/spec/support/finders/test_selector_finders.rb @@ -48,6 +48,10 @@ module TestSelectorFinders def have_test_selector(value, **) have_selector(test_selector(value), **) end + + def have_no_test_selector(value, **) + have_no_selector(test_selector(value), **) + end end RSpec.configure do |config|