Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 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, see <https://www.gnu.org/licenses/>.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Saml
module Providers
module MetadataChanges
class RowComponent < ::OpPrimer::BorderBoxRowComponent
delegate :provider, to: :table

def attribute
I18n.t("activerecord.attributes.saml/provider.#{key}")
end

def current_value
current = provider.options_in_database[key]
return I18n.t(:label_none_parentheses) if current.blank?

simple_format(h(current.to_s))
end

def new_value
simple_format(h(value.to_s))
end

private

def key = row.first

def value = row.last
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 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, see <https://www.gnu.org/licenses/>.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Saml
module Providers
module MetadataChanges
class TableComponent < ::OpPrimer::BorderBoxTableComponent
options :provider

columns :attribute, :current_value, :new_value
main_column :attribute, :current_value, :new_value
mobile_labels :current_value, :new_value

def row_class
RowComponent
end

def mobile_title
t(".mobile_title")
end

def headers
[
[:attribute, { caption: I18n.t(:label_attribute) }],
[:current_value, { caption: t(".current_value") }],
[:new_value, { caption: t(".new_value") }]
]
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<%=
render(Primer::OpenProject::SidePanel::Section.new(test_selector: "saml_provider_refresh_metadata")) do |section|
section.with_title { I18n.t("saml.providers.label_metadata") }
section.with_description { t(".description") }

render(
Primer::Beta::Button.new(
tag: :a,
href: refresh_metadata_saml_provider_path(provider),
data: { turbo_prefetch: false }
)
) { t(".button") }
end
%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 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, see <https://www.gnu.org/licenses/>.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Saml::Providers
module SidePanel
class RefreshMetadataComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers

alias_method :provider, :model

def render?
provider.persisted? && provider.metadata_url.present? && !provider.seeded_from_env?
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
render(Primer::OpenProject::SidePanel.new(spacious: true)) do |panel|
[
Saml::Providers::SidePanel::MetadataComponent.new(@provider),
Saml::Providers::SidePanel::InformationComponent.new(@provider)
Saml::Providers::SidePanel::InformationComponent.new(@provider),
Saml::Providers::SidePanel::RefreshMetadataComponent.new(@provider)
].each do |component|
panel.with_section(component)
end
Expand Down
73 changes: 71 additions & 2 deletions modules/auth_saml/app/controllers/saml/providers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ class ProvidersController < ::ApplicationController

before_action :require_admin
before_action :check_ee, except: %i[index]
before_action :find_provider, only: %i[show edit import_metadata update confirm_destroy destroy]
before_action :check_provider_writable, only: %i[update import_metadata]
before_action :find_provider,
only: %i[show edit import_metadata refresh_metadata apply_metadata update confirm_destroy destroy]
before_action :check_provider_writable, only: %i[update import_metadata refresh_metadata apply_metadata]
before_action :check_metadata_url, only: %i[refresh_metadata apply_metadata]
before_action :set_edit_state, only: %i[create edit update import_metadata]

def index
Expand Down Expand Up @@ -97,6 +99,29 @@ def create
end
end

def refresh_metadata
call = Saml::UpdateMetadataService.new(provider: @provider, user: User.current).call

if call.success?
prepare_metadata_preview
else
flash[:error] = call.message
redirect_to saml_provider_path(@provider)
end
end

def apply_metadata
preview = metadata_verifier.verified(params[:metadata_token], purpose: metadata_token_purpose)

if (error = metadata_preview_error(preview))
flash[:error] = error
else
apply_metadata_options(preview["options"])
end

redirect_to saml_provider_path(@provider)
end

def update
call = Saml::Providers::UpdateService
.new(model: @provider, user: User.current)
Expand Down Expand Up @@ -131,6 +156,50 @@ def destroy

private

def prepare_metadata_preview
@metadata_changes = @provider.options.filter do |key, value|
key != "last_metadata_update" && value != @provider.options_in_database[key]
end
@metadata_token = metadata_verifier.generate(
{
"provider_version" => @provider.cache_key_with_version,
"options" => @metadata_changes.merge("last_metadata_update" => @provider.last_metadata_update)
},
purpose: metadata_token_purpose,
expires_in: 15.minutes
)
end

def metadata_preview_error(preview)
if preview.nil?
t(".expired")
elsif preview["provider_version"] != @provider.cache_key_with_version
t(".provider_changed")
end
end

def apply_metadata_options(options)
call = Saml::Providers::UpdateService.new(model: @provider, user: User.current).call(options:)

if call.success?
flash[:notice] = I18n.t("saml.metadata_parser.success")
else
flash[:error] = call.message
end
end

def check_metadata_url
render_404 if @provider.metadata_url.blank?
end

def metadata_verifier
Rails.application.message_verifier("saml_metadata_refresh")
end

def metadata_token_purpose
"apply_metadata/#{@provider.id}"
end

def successful_save_response
if @new_mode && !@next_edit_state
flash[:notice] = I18n.t("saml.providers.notice_created")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,11 @@ def call # rubocop:disable Metrics/AbcSize
private

def apply_metadata(metadata)
new_options = provider.options.merge(metadata)
last_metadata_update = metadata.blank? ? nil : Time.current

Saml::Providers::SetAttributesService
.new(model: @provider, user: User.current, contract_class: Saml::Providers::UpdateContract)
.call({ options: new_options, last_metadata_update: })
.call({ options: metadata, last_metadata_update: })
end

def fetch_metadata
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<% html_title(t(:label_administration), t(".title"), @provider.display_name) %>

<% content_for :header_tags do %>
<meta name="turbo-cache-control" content="no-cache">
<% end %>

<%= render(Primer::OpenProject::PageHeader.new) do |header| %>
<% header.with_title { t(".title") } %>
<% header.with_breadcrumbs(
[{ href: admin_index_path, text: t(:label_administration) },
{ href: saml_providers_path, text: t("saml.providers.plural") },
{ href: saml_provider_path(@provider), text: @provider.display_name },
t(".title")]
) %>
<% header.with_description { t(".description") } %>
<% end %>

<% if @metadata_changes.empty? %>
<%= render(Primer::Alpha::Banner.new) { t(".no_changes") } %>
<% else %>
<%= render(Saml::Providers::MetadataChanges::TableComponent.new(rows: @metadata_changes.to_a, provider: @provider)) %>
<% end %>

<%= form_with(url: apply_metadata_saml_provider_path(@provider), method: :post) do %>
<%= hidden_field_tag :metadata_token, @metadata_token %>
<%= flex_layout(align_items: :center, mt: 3) do |actions|
actions.with_column(mr: 2) do
render(Primer::Beta::Button.new(type: :submit, scheme: :primary)) { t(".apply") }
end
actions.with_column do
render(Primer::Beta::Button.new(tag: :a, href: saml_provider_path(@provider))) { t(:button_cancel) }
end
end %>
<% end %>
18 changes: 18 additions & 0 deletions modules/auth_saml/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ en:
icon: "Custom icon"
identifier: Identifier
idp_cert: Public certificate of identity provider
idp_cert_fingerprint: Identity provider certificate fingerprint
idp_entity_id: Identity provider entity ID
idp_slo_service_url: Identity provider logout endpoint
idp_sso_service_url: Identity provider login endpoint
Expand Down Expand Up @@ -128,6 +129,9 @@ en:
metadata_too_large: "The metadata file exceeds the maximum allowed size."
success: "Successfully updated the configuration using the identity provider metadata."
providers:
apply_metadata:
expired: "This metadata preview has expired or is invalid. Refresh the metadata again to review the latest configuration."
provider_changed: "The provider configuration has changed since this preview. Refresh the metadata again to review the latest configuration."
attribute_mapping: Attribute mapping
attribute_mapping_text: >
The following fields control which attributes provided by the SAML identity provider
Expand All @@ -147,10 +151,20 @@ en:
label_uid: Internal user id
metadata:
dialog: "This is the URL where the OpenProject SAML metadata is available. Optionally use it to configure your identity provider:"
metadata_changes:
table_component:
current_value: "Current value"
mobile_title: "Metadata changes"
new_value: "New value"

no_results_table: No SAML identity providers have been defined yet.
notice_created: A new SAML identity provider was successfully created.
plural: SAML identity providers
refresh_metadata:
apply: "Apply changes"
description: "Review the configuration fetched from your identity provider. Changes are saved only when you apply them."
no_changes: "The metadata matches the current configuration."
title: "Review metadata changes"
request_attributes:
format: 'Attribute format'
legend: >
Expand All @@ -174,6 +188,10 @@ en:
metadata_form_banner: "Editing the metadata may override existing values in other sections. "
requested_attributes: "Define the set of attributes to be requested in the SAML request sent to your identity provider."
seeded_from_env: "This provider was seeded from the environment configuration. It cannot be edited."
side_panel:
refresh_metadata_component:
button: "Refresh metadata"
description: "You can check if the configuration fetched from your identity provider has changed."
singular: SAML identity provider
upsell:
description: Connect OpenProject to a SAML identity provider
Expand Down
2 changes: 2 additions & 0 deletions modules/auth_saml/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
resources :providers do
member do
post :import_metadata
get :refresh_metadata
post :apply_metadata
get :confirm_destroy
end
end
Expand Down
Loading
Loading