Skip to content

Commit

Permalink
Merge pull request #18018 from opf/use-idp-audience
Browse files Browse the repository at this point in the history
Allow admins to choose not exchanging for specific SSO token
  • Loading branch information
NobodysNightmare authored Feb 25, 2025
2 parents 8bc0c6b + 74bad37 commit 8b3ae66
Show file tree
Hide file tree
Showing 21 changed files with 253 additions and 103 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* -- 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 StorageAudienceController extends Controller {
static targets = [
'audienceInput',
'audienceInputWrapper',
'idpRadio',
];

declare readonly audienceInputTarget:HTMLInputElement;
declare readonly audienceInputWrapperTarget:HTMLDivElement;
declare readonly idpRadioTarget:HTMLInputElement;

connect() {
if (this.idpRadioTarget.checked) {
this.hideAudienceInput();
}
}

showAudienceInput() {
this.audienceInputWrapperTarget.classList.remove('d-none');
}

hideAudienceInput() {
this.audienceInputWrapperTarget.classList.add('d-none');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ module Peripherals
namespace("forms") do
register(:automatically_managed_folders, ::Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent)
register(:general_information, ::Storages::Admin::Forms::GeneralInfoFormComponent)
register(:nextcloud_audience, ::Storages::Admin::Forms::NextcloudAudienceFormComponent)
register(:storage_audience, ::Storages::Admin::Forms::NextcloudAudienceFormComponent)
register(:oauth_application, ::Storages::Admin::OAuthApplicationInfoCopyComponent)
register(:oauth_client, ::Storages::Admin::Forms::OAuthClientFormComponent)
end
Expand All @@ -70,15 +70,15 @@ module Peripherals

register(:automatically_managed_folders, ::Storages::Admin::AutomaticallyManagedProjectFoldersInfoComponent)
register(:general_information, ::Storages::Admin::GeneralInfoComponent)
register(:nextcloud_audience, ::Storages::Admin::NextcloudAudienceInfoComponent)
register(:storage_audience, ::Storages::Admin::NextcloudAudienceInfoComponent)
register(:oauth_application, ::Storages::Admin::OAuthApplicationInfoComponent)
register(:oauth_client, ::Storages::Admin::OAuthClientInfoComponent)
end

namespace("contracts") do
register(:storage, ::Storages::Storages::NextcloudContract)
register(:general_information, ::Storages::Storages::NextcloudGeneralInformationContract)
register(:nextcloud_audience, ::Storages::Storages::NextcloudAudienceContract)
register(:storage_audience, ::Storages::Storages::NextcloudAudienceContract)
end

namespace("models") do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ class NextcloudStorageWizard < Wizard

# OAuth 2.0 SSO

step :nextcloud_audience,
step :storage_audience,
section: :oauth_configuration,
if: ->(storage) { storage.authenticate_via_idp? },
completed_if: ->(storage) { storage.nextcloud_audience.present? }
completed_if: ->(storage) { storage.storage_audience.present? }

# Two-Way OAuth 2.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ See COPYRIGHT and LICENSE files for more details.

<%=
component_wrapper(tag: "turbo-frame") do
render(Primer::Beta::Text.new(tag: :div, test_selector: "storage-nextcloud-audience-form")) do
render(Primer::Beta::Text.new(tag: :div, test_selector: "storage-audience-form")) do
primer_form_with(
model:,
url: form_url,
method: :patch,
data: { turbo_frame: "page-content" }
data: {
turbo_frame: "page-content",
application_target: "dynamic",
controller: "storages--storage-audience"
}
) do |form|
flex_layout do |general_info_row|
general_info_row.with_row(mb: 3) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
#
module Storages::Admin::Forms
class NextcloudAudienceFormComponent < StorageFormComponent
def self.wrapper_key = :storage_nextcloud_audience_section
def self.wrapper_key = :storage_audience_section

options submit_button_disabled: false

def form_url
query = { origin_component: "nextcloud_audience" }
query = { origin_component: "storage_audience" }
query[:continue_wizard] = storage.id if in_wizard

admin_settings_storage_path(storage, query)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,12 @@ See COPYRIGHT and LICENSE files for more details.
component_wrapper(tag: 'turbo-frame') do
grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid|
grid.with_area(:item, tag: :div, mr: 3) do
concat(render(Primer::Beta::Text.new(font_weight: :bold, mr: 1, test_selector: 'nextcloud-audience-label')) { I18n.t('storages.file_storage_view.nextcloud_audience') })
concat(configuration_check_label_for(:nextcloud_audience_configured))
concat(render(Primer::Beta::Text.new(font_weight: :bold, mr: 1, test_selector: 'storage-audience-label')) { I18n.t('storages.file_storage_view.storage_audience') })
concat(configuration_check_label_for(:storage_audience_configured))
end

grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'nextcloud-audience-description') do
concat(render(Primer::Beta::Text.new) {
if storage.nextcloud_audience.present?
I18n.t('storages.file_storage_view.nextcloud_audience_description', audience: storage.nextcloud_audience)
else
I18n.t('storages.file_storage_view.nextcloud_audience_blank')
end
})
grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-audience-description') do
concat(render(Primer::Beta::Text.new) { audience_summary })
end

if editable_storage?
Expand All @@ -54,9 +48,9 @@ See COPYRIGHT and LICENSE files for more details.
icon: :pencil,
tag: :a,
scheme: :invisible,
href: edit_nextcloud_audience_admin_settings_storage_path(storage),
aria: { label: I18n.t('storages.label_edit_nextcloud_audience') },
test_selector: 'storage-edit-nextcloud-audience-button',
href: edit_storage_audience_admin_settings_storage_path(storage),
aria: { label: I18n.t('storages.label_edit_storage_audience') },
test_selector: 'storage-edit-storage-audience-button',
data: { turbo_stream: true }
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,18 @@
module Storages
module Admin
class NextcloudAudienceInfoComponent < StorageInfoComponent
def self.wrapper_key = :storage_nextcloud_audience_section
def self.wrapper_key = :storage_audience_section

def audience_summary
case storage.storage_audience
when ""
I18n.t("storages.file_storage_view.storage_audience_blank")
when OpenIDConnect::UserToken::IDP_AUDIENCE
I18n.t("storages.file_storage_view.storage_audience_idp")
else
I18n.t("storages.file_storage_view.storage_audience_description", audience: storage.storage_audience)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@

module Storages::Storages
class NextcloudAudienceContract < ::ModelContract
attribute :nextcloud_audience
validates :nextcloud_audience, presence: true, if: -> { nextcloud_storage_authenticate_via_idp? }
attribute :storage_audience
validates :storage_audience, presence: true, if: -> { nextcloud_storage_authenticate_via_idp? }

# Adding this to allow writing the nextcloud_audience
# Adding this to allow writing the storage_audience
attribute :provider_fields

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Storages::Admin::StoragesController < ApplicationController
# and set the @<controller_name> variable to the object referenced in the URL.
before_action :require_admin
before_action :find_model_object,
only: %i[show_oauth_application destroy edit edit_host edit_nextcloud_audience confirm_destroy update
only: %i[show_oauth_application destroy edit edit_host edit_storage_audience confirm_destroy update
change_health_notifications_enabled replace_oauth_application]
before_action :ensure_valid_wizard_parameters, only: [:new]
before_action :require_ee_token_for_one_drive, only: [:new]
Expand Down Expand Up @@ -123,7 +123,7 @@ def edit_host
respond_with_turbo_streams
end

def edit_nextcloud_audience
def edit_storage_audience
update_via_turbo_stream(component: Storages::Admin::Forms::NextcloudAudienceFormComponent.new(@storage))
respond_with_turbo_streams
end
Expand Down Expand Up @@ -252,7 +252,8 @@ def permitted_storage_params(model_parameter_name = storage_provider_parameter_n
"provider_type",
"host",
"authentication_method",
"nextcloud_audience",
"audience_configuration",
"storage_audience",
"oauth_client_id",
"oauth_client_secret",
"tenant_id",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,48 @@
module Storages::Admin
class NextcloudAudienceInputForm < ApplicationForm
form do |storage_form|
storage_form.text_field(
name: :nextcloud_audience,
label: I18n.t("activerecord.attributes.storages/nextcloud_storage.nextcloud_audience"),
required: true,
caption: I18n.t("storages.instructions.nextcloud.nextcloud_audience"),
placeholder: I18n.t("storages.instructions.nextcloud.nextcloud_audience_placeholder"),
input_width: :large
)
storage_form.radio_button_group(name: :audience_configuration) do |group|
group.radio_button(
value: :idp,
checked: idp?,
label: I18n.t("storages.storage_audience.idp.label"),
caption: I18n.t("storages.storage_audience.idp.helptext"),
data: { action: "storages--storage-audience#hideAudienceInput", "storages--storage-audience-target": "idpRadio" }
)

group.radio_button(
value: :manual,
checked: !idp?,
label: I18n.t("storages.storage_audience.manual.label"),
caption: I18n.t("storages.storage_audience.manual.helptext"),
data: { action: "storages--storage-audience#showAudienceInput" }
)
end

storage_form.group(data: { "storages--storage-audience-target": "audienceInputWrapper" }) do |toggleable_group|
toggleable_group.text_field(
name: :storage_audience,
label: I18n.t("activerecord.attributes.storages/nextcloud_storage.storage_audience"),
required: true,
caption: I18n.t("storages.instructions.nextcloud.storage_audience"),
placeholder: I18n.t("storages.instructions.nextcloud.storage_audience_placeholder"),
input_width: :large,
data: { "storages--storage-audience-target": "audienceInput" },
value: prefilled_audience
)
end
end

private

def idp?
model.storage_audience == OpenIDConnect::UserToken::IDP_AUDIENCE
end

def prefilled_audience
return "" if idp?

model.storage_audience
end
end
end
6 changes: 3 additions & 3 deletions modules/storages/app/models/storages/nextcloud_storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class NextcloudStorage < Storage
store_attribute :provider_fields, :group, :string
store_attribute :provider_fields, :group_folder, :string
store_attribute :provider_fields, :authentication_method, :string, default: "two_way_oauth2"
store_attribute :provider_fields, :nextcloud_audience, :string
store_attribute :provider_fields, :storage_audience, :string

def oauth_configuration
Peripherals::OAuthConfigurations::NextcloudConfiguration.new(self)
Expand All @@ -67,7 +67,7 @@ def available_project_folder_modes
end

def audience
nextcloud_audience
storage_audience
end

def authenticate_via_idp?
Expand All @@ -83,7 +83,7 @@ def configuration_checks
storage_oauth_client_configured: !authenticate_via_storage? || oauth_client.present?,
openproject_oauth_application_configured: !authenticate_via_storage? || oauth_application.present?,
host_name_configured: host.present? && name.present?,
nextcloud_audience_configured: !authenticate_via_idp? || nextcloud_audience.present?
storage_audience_configured: !authenticate_via_idp? || storage_audience.present?
}
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ def set_default_attributes(_params)
private

def set_attributes(params)
audience_config = params.delete(:audience_configuration)

super

unset_nextcloud_application_credentials if nextcloud_storage?
set_idp_audience if audience_config == "idp"
end

def sanitize_host
Expand Down Expand Up @@ -72,5 +76,9 @@ def storage
def nextcloud_storage?
storage.is_a?(Storages::NextcloudStorage)
end

def set_idp_audience
storage.storage_audience = OpenIDConnect::UserToken::IDP_AUDIENCE
end
end
end
Loading

0 comments on commit 8b3ae66

Please sign in to comment.