Skip to content

Commit 8bb6775

Browse files
Merge pull request opf#16935 from opf/feature/57677-oidc-ui
Update OIDC configuration UI
2 parents 6afa6b9 + 6ac6af4 commit 8bb6775

File tree

90 files changed

+4052
-868
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+4052
-868
lines changed

.rubocop.yml

+1
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ RSpec/DescribeMethod:
248248
# to match the exact file name
249249
RSpec/SpecFilePathFormat:
250250
CustomTransform:
251+
OpenIDConnect: openid_connect
251252
OAuthClients: oauth_clients
252253
IgnoreMethods: true
253254

app/components/op_primer/border_box_table_component.html.erb

+5-5
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ See COPYRIGHT and LICENSE files for more details.
4646

4747
if rows.empty?
4848
component.with_row(scheme: :default) { render_blank_slate }
49-
end
50-
51-
rows.each do |row|
52-
component.with_row(scheme: :default) do
53-
render(row_class.new(row:, table: self))
49+
else
50+
rows.each do |row|
51+
component.with_row(scheme: :default) do
52+
render(row_class.new(row:, table: self))
53+
end
5454
end
5555
end
5656
end

app/mailers/user_mailer.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def password_change_not_possible(user)
7272
if user.ldap_auth_source
7373
user.ldap_auth_source.name
7474
else
75-
user.authentication_provider
75+
user.human_authentication_provider
7676
end
7777
open_project_headers "Type" => "Account"
7878

app/models/auth_provider.rb

+18
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,20 @@ class AuthProvider < ApplicationRecord
3232
validates :display_name, presence: true
3333
validates :display_name, uniqueness: true
3434

35+
after_destroy :unset_direct_provider
36+
3537
def self.slug_fragment
3638
raise NotImplementedError
3739
end
3840

41+
def user_count
42+
@user_count ||= User.where("identity_url LIKE ?", "#{slug}%").count
43+
end
44+
45+
def human_type
46+
raise NotImplementedError
47+
end
48+
3949
def auth_url
4050
root_url = OpenProject::StaticRouting::StaticUrlHelpers.new.root_url
4151
URI.join(root_url, "auth/#{slug}/").to_s
@@ -44,4 +54,12 @@ def auth_url
4454
def callback_url
4555
URI.join(auth_url, "callback").to_s
4656
end
57+
58+
protected
59+
60+
def unset_direct_provider
61+
if Setting.omniauth_direct_login_provider == slug
62+
Setting.omniauth_direct_login_provider = ""
63+
end
64+
end
4765
end

app/models/user.rb

+6-1
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,12 @@ def name(formatter = nil)
308308
def authentication_provider
309309
return if identity_url.blank?
310310

311-
identity_url.split(":", 2).first.titleize
311+
identity_url.split(":", 2).first
312+
end
313+
314+
# Return user's authentication provider for display
315+
def human_authentication_provider
316+
authentication_provider&.titleize
312317
end
313318

314319
##

app/views/admin/settings/authentication_settings/show.html.erb

+21
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,27 @@ See COPYRIGHT and LICENSE files for more details.
5757
<%= render Settings::NumericSettingComponent.new("invitation_expiration_days", unit: "days") %>
5858
</fieldset>
5959

60+
<fieldset class="form--fieldset">
61+
<legend class="form--fieldset-legend"><%= I18n.t(:'settings.authentication.single_sign_on') %></legend>
62+
<div class="form--field">
63+
<% providers = AuthProvider
64+
.where(available: true)
65+
.order("lower(display_name) ASC")
66+
.select(:type, :display_name, :slug)
67+
.to_a
68+
.map { |p| ["#{p.display_name} (#{p.human_type})", p.slug] }
69+
%>
70+
<%= setting_select :omniauth_direct_login_provider,
71+
[[t(:label_disabled), ""]] + providers,
72+
container_class: '-middle' %>
73+
<span class="form--field-instructions">
74+
<%= t("settings.authentication.omniauth_direct_login_hint_html",
75+
internal_path: internal_signin_url) %>
76+
</span>
77+
</div>
78+
</fieldset>
79+
80+
6081
<fieldset class="form--fieldset">
6182
<fieldset id="registration_footer" class="form--fieldset">
6283
<legend class="form--fieldset-legend"><%= I18n.t(:setting_registration_footer) %></legend>

app/views/users/form/authentication/_external.html.erb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div class="form--field -reduced-margin">
22
<%= styled_label_tag nil, I18n.t('user.authentication_provider') %>
33
<div class="form--field-container">
4-
<%= @user.authentication_provider %>
4+
<%= @user.human_authentication_provider %>
55
</div>
66
</div>
77
<div class="form--field-instructions">

config/initializers/zeitwerk.rb

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"OAuth#{default_inflect($1, abspath)}"
3535
when /\A(.*)_oauth\z/
3636
"#{default_inflect($1, abspath)}OAuth"
37+
when "openid_connect"
38+
"OpenIDConnect"
3739
when "oauth"
3840
"OAuth"
3941
when /\Aclamav_(.*)\z/

config/locales/en.yml

+13
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,7 @@ en:
962962
not_a_datetime: "is not a valid date time."
963963
not_a_number: "is not a number."
964964
not_allowed: "is invalid because of missing permissions."
965+
not_json: "is not a valid JSON object."
965966
not_an_integer: "is not an integer."
966967
not_an_iso_date: "is not a valid date. Required format: YYYY-MM-DD."
967968
not_same_project: "doesn't belong to the same project."
@@ -2141,6 +2142,7 @@ en:
21412142
label_api_doc: "API documentation"
21422143
label_backup: "Backup"
21432144
label_backup_code: "Backup code"
2145+
label_basic_details: "Basic details"
21442146
label_between: "between"
21452147
label_blocked_by: "blocked by"
21462148
label_blocks: "blocks"
@@ -2374,6 +2376,7 @@ en:
23742376
label_custom_favicon: "Custom favicon"
23752377
label_custom_touch_icon: "Custom touch icon"
23762378
label_logout: "Sign out"
2379+
label_mapping_for: "Mapping for: %{attribute}"
23772380
label_main_menu: "Side Menu"
23782381
label_manage: "Manage"
23792382
label_manage_groups: "Manage groups"
@@ -3340,7 +3343,9 @@ en:
33403343
setting_default_language: "Default language"
33413344
setting_default_projects_modules: "Default enabled modules for new projects"
33423345
setting_default_projects_public: "New projects are public by default"
3346+
setting_disable_password_login: "Disable password authentication"
33433347
setting_diff_max_lines_displayed: "Max number of diff lines displayed"
3348+
setting_omniauth_direct_login_provider: "Direct login SSO provider"
33443349
setting_display_subprojects_work_packages: "Display subprojects work packages on main projects by default"
33453350
setting_duration_format: "Duration format"
33463351
setting_duration_format_hours_only: "Hours only"
@@ -3444,6 +3449,14 @@ en:
34443449
setting_working_days: "Working days"
34453450

34463451
settings:
3452+
authentication:
3453+
single_sign_on: "Single Sign-On"
3454+
omniauth_direct_login_hint_html: >
3455+
If this option is active, login requests will redirect to the configured omniauth provider.
3456+
The login dropdown and sign-in page will be disabled.
3457+
<br/>
3458+
<strong>Note:</strong> Unless you also disable password logins, with this option enabled,
3459+
users can still log in internally by visiting the <code>%{internal_path}</code> login page.
34473460
attachments:
34483461
whitelist_text_html: >
34493462
Define a list of valid file extensions and/or mime types for uploaded files.

lib/open_project/static/links.rb

+9
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,15 @@ def static_links
273273
sysadmin_docs: {
274274
saml: {
275275
href: "https://www.openproject.org/docs/system-admin-guide/authentication/saml/"
276+
},
277+
oidc: {
278+
href: "https://www.openproject.org/docs/installation-and-operations/misc/custom-openid-connect-providers/"
279+
},
280+
oidc_claims: {
281+
href: "https://www.openproject.org/docs/installation-and-operations/misc/custom-openid-connect-providers/#claims"
282+
},
283+
oidc_acr_values: {
284+
href: "https://www.openproject.org/docs/installation-and-operations/misc/custom-openid-connect-providers/#non-essential-claims"
276285
}
277286
},
278287
storage_docs: {

modules/auth_plugins/app/views/hooks/login/_providers.html.erb

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details.
2727
2828
++#%>
2929

30-
<% OpenProject::Plugins::AuthPlugin.providers.each do |pro| %>
30+
<% OpenProject::Plugins::AuthPlugin.providers.each do |provider| %>
3131
<%
3232
opts = { script_name: OpenProject::Configuration.rails_relative_url_root }
3333

@@ -36,8 +36,8 @@ See COPYRIGHT and LICENSE files for more details.
3636
end
3737
%>
3838
<a
39-
href="<%= omni_auth_start_path(pro[:name], opts) %>"
40-
class="auth-provider auth-provider-<%= pro[:name] %> <%= pro[:icon] ? 'auth-provider--imaged' : '' %> button">
41-
<span class="auth-provider-name"><%= pro[:display_name] || pro[:name] %></span>
39+
href="<%= omni_auth_start_path(provider[:name], opts) %>"
40+
class="auth-provider auth-provider-<%= provider[:name] %> <%= provider[:icon] ? 'auth-provider--imaged' : '' %> button">
41+
<span class="auth-provider-name"><%= provider[:display_name] || provider[:name] %></span>
4242
</a>
4343
<% end %>

modules/auth_plugins/app/views/hooks/login/_providers_css.html.erb

+8-8
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@ See COPYRIGHT and LICENSE files for more details.
2727
2828
++#%>
2929

30-
<% OpenProject::Plugins::AuthPlugin.providers.each do |pro| %>
31-
<% if pro[:icon] %>
30+
<% OpenProject::Plugins::AuthPlugin.providers.each do |provider| %>
31+
<% if provider[:icon] %>
3232
<style type="text/css">
33-
#content .login-auth-providers a.auth-provider.auth-provider-<%= pro[:name] %> {
34-
background-image: url('<%= asset_path(pro[:icon]) %>');
33+
#content .login-auth-providers a.auth-provider.auth-provider-<%= provider[:name] %> {
34+
background-image: url('<%= asset_path(provider[:icon]) %>');
3535
}
36-
.op-app-header #nav-login-content .login-auth-providers a.auth-provider.auth-provider-<%= pro[:name] %> {
37-
background-image: url('<%= asset_path(pro[:icon]) %>') ;
36+
.op-app-header #nav-login-content .login-auth-providers a.auth-provider.auth-provider-<%= provider[:name] %> {
37+
background-image: url('<%= asset_path(provider[:icon]) %>') ;
3838
}
39-
.login-auth-providers a.auth-provider.auth-provider-<%= pro[:name] %> {
40-
background-image: url('<%= asset_path(pro[:icon]) %>') ;
39+
.login-auth-providers a.auth-provider.auth-provider-<%= provider[:name] %> {
40+
background-image: url('<%= asset_path(provider[:icon]) %>') ;
4141
}
4242
</style>
4343
<% end -%>

modules/auth_saml/app/components/saml/providers/row_component.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def edit_link
5353
end
5454

5555
def users
56-
User.where("identity_url LIKE ?", "#{provider.slug}%").count.to_s
56+
provider.user_count.to_s
5757
end
5858

5959
def creator

modules/auth_saml/app/controllers/saml/providers_controller.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class ProvidersController < ::ApplicationController
77

88
before_action :require_admin
99
before_action :check_ee
10-
before_action :find_provider, only: %i[show edit import_metadata update destroy]
10+
before_action :find_provider, only: %i[show edit import_metadata update confirm_destroy destroy]
1111
before_action :check_provider_writable, only: %i[update import_metadata]
1212
before_action :set_edit_state, only: %i[create edit update import_metadata]
1313

@@ -85,7 +85,7 @@ def create
8585
def update
8686
call = Saml::Providers::UpdateService
8787
.new(model: @provider, user: User.current)
88-
.call(options: update_params)
88+
.call(update_params)
8989

9090
if call.success?
9191
flash[:notice] = I18n.t(:notice_successful_update) unless @edit_mode
@@ -96,6 +96,8 @@ def update
9696
end
9797
end
9898

99+
def confirm_destroy; end
100+
99101
def destroy
100102
call = ::Saml::Providers::DeleteService
101103
.new(model: @provider, user: User.current)

modules/auth_saml/app/forms/saml/providers/mapping_form.rb

+5-5
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class MappingForm < BaseForm
3232
form do |f|
3333
f.text_area(
3434
name: :mapping_login,
35-
label: I18n.t("saml.providers.label_mapping_for", attribute: User.human_attribute_name(:login)),
35+
label: I18n.t("label_mapping_for", attribute: User.human_attribute_name(:login)),
3636
caption: I18n.t("saml.instructions.mapping_login"),
3737
required: true,
3838
disabled: provider.seeded_from_env?,
@@ -41,7 +41,7 @@ class MappingForm < BaseForm
4141
)
4242
f.text_area(
4343
name: :mapping_mail,
44-
label: I18n.t("saml.providers.label_mapping_for", attribute: User.human_attribute_name(:mail)),
44+
label: I18n.t("label_mapping_for", attribute: User.human_attribute_name(:mail)),
4545
caption: I18n.t("saml.instructions.mapping_mail"),
4646
required: true,
4747
disabled: provider.seeded_from_env?,
@@ -50,7 +50,7 @@ class MappingForm < BaseForm
5050
)
5151
f.text_area(
5252
name: :mapping_firstname,
53-
label: I18n.t("saml.providers.label_mapping_for", attribute: User.human_attribute_name(:first_name)),
53+
label: I18n.t("label_mapping_for", attribute: User.human_attribute_name(:first_name)),
5454
caption: I18n.t("saml.instructions.mapping_firstname"),
5555
required: true,
5656
disabled: provider.seeded_from_env?,
@@ -59,7 +59,7 @@ class MappingForm < BaseForm
5959
)
6060
f.text_area(
6161
name: :mapping_lastname,
62-
label: I18n.t("saml.providers.label_mapping_for", attribute: User.human_attribute_name(:last_name)),
62+
label: I18n.t("label_mapping_for", attribute: User.human_attribute_name(:last_name)),
6363
caption: I18n.t("saml.instructions.mapping_lastname"),
6464
required: true,
6565
disabled: provider.seeded_from_env?,
@@ -68,7 +68,7 @@ class MappingForm < BaseForm
6868
)
6969
f.text_field(
7070
name: :mapping_uid,
71-
label: I18n.t("saml.providers.label_mapping_for", attribute: I18n.t("saml.providers.label_uid")),
71+
label: I18n.t("label_mapping_for", attribute: I18n.t("saml.providers.label_uid")),
7272
caption: I18n.t("saml.instructions.mapping_uid"),
7373
disabled: provider.seeded_from_env?,
7474
rows: 8,

modules/auth_saml/app/models/saml/provider.rb

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class Provider < AuthProvider
4545

4646
def self.slug_fragment = "saml"
4747

48+
def human_type
49+
"SAML"
50+
end
51+
4852
def seeded_from_env?
4953
(Setting.seed_saml_provider || {}).key?(slug)
5054
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<%#-- copyright
2+
OpenProject is an open source project management software.
3+
Copyright (C) the OpenProject GmbH
4+
5+
This program is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU General Public License version 3.
7+
8+
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
9+
Copyright (C) 2006-2013 Jean-Philippe Lang
10+
Copyright (C) 2010-2013 the ChiliProject Team
11+
12+
This program is free software; you can redistribute it and/or
13+
modify it under the terms of the GNU General Public License
14+
as published by the Free Software Foundation; either version 2
15+
of the License, or (at your option) any later version.
16+
17+
This program is distributed in the hope that it will be useful,
18+
but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
GNU General Public License for more details.
21+
22+
You should have received a copy of the GNU General Public License
23+
along with this program; if not, write to the Free Software
24+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25+
26+
See COPYRIGHT and LICENSE files for more details.
27+
28+
++#%>
29+
<%= styled_form_tag(saml_provider_path(@provider),
30+
class: 'danger-zone',
31+
method: :delete) do %>
32+
<section class="form--section">
33+
<h3 class="form--section-title">
34+
<%= t('saml.delete_title') %>
35+
</h3>
36+
<p>
37+
<%= t('provider.delete_warning.provider', name: content_tag(:strong, @provider.display_name)).html_safe %>
38+
</p>
39+
<ul class="mb-3">
40+
<li> <%= t('provider.delete_warning.delete_result_1') %>
41+
<li> <%= t('provider.delete_warning.delete_result_user_count', count: @provider.user_count) %>
42+
<% if Setting.omniauth_direct_login_provider == @provider.slug %>
43+
<li> <%= t('provider.delete_warning.delete_result_direct') %>
44+
<% end %>
45+
</ul>
46+
<p class="danger-zone--warning">
47+
<span class="icon icon-error"></span>
48+
<span><%= t('provider.delete_warning.irreversible_notice') %></span>
49+
</p>
50+
<p>
51+
<%= t('provider.delete_warning.input_delete_confirmation', name: "<em class=\"danger-zone--expected-value\">#{h(@provider.display_name)}</em>").html_safe %>
52+
</p>
53+
<div class="danger-zone--verification">
54+
<%= text_field_tag :delete_confirmation %>
55+
<%= styled_button_tag title: t(:button_delete), class: '-primary', disabled: true do
56+
concat content_tag :i, '', class: 'button--icon icon-delete'
57+
concat content_tag :span, t(:button_delete), class: 'button--text'
58+
end %>
59+
<%= link_to saml_providers_path,
60+
title: t(:button_cancel),
61+
class: 'button -with-icon icon-cancel' do %>
62+
<%= t(:button_cancel) %>
63+
<% end %>
64+
</div>
65+
</section>
66+
<% end %>

0 commit comments

Comments
 (0)