allow to limit self registration for openid connect providers

This commit is contained in:
Markus Kahl
2023-04-19 15:48:54 +02:00
parent 1130c136c3
commit faafa84608
13 changed files with 386 additions and 18 deletions
+5 -1
View File
@@ -104,7 +104,7 @@ module Users
# Try to register a user with an existsing omniauth connection
# bypassing regular account registration restrictions
def register_omniauth_user
return if user.identity_url.blank?
return if skip_omniauth_user?
user.activate
@@ -113,6 +113,10 @@ module Users
end
end
def skip_omniauth_user?
user.identity_url.blank?
end
def register_by_email_activation
return unless Setting::SelfRegistration.by_email?
@@ -38,6 +38,8 @@ module OpenProject::AuthPlugins
author_url: 'https://www.openproject.org',
bundled: true
patch_with_namespace :Users, :RegisterUserService
config.to_prepare do
OpenProject::AuthPlugins::Hooks
end
@@ -0,0 +1,30 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 OpenProject::AuthPlugins::Patches
end
@@ -0,0 +1,45 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 OpenProject::AuthPlugins::Patches::RegisterUserServicePatch
def self.included(base) # :nodoc:
base.prepend InstanceMethods
end
module InstanceMethods
def skip_omniauth_user?
super || limit_self_registration?(user)
end
def limit_self_registration?(user)
provider = user.authentication_provider.downcase
OpenProject::Plugins::AuthPlugin.limit_self_registration? provider:
end
end
end
@@ -92,6 +92,15 @@ module OpenProject::Plugins
[camelization, name].compact.first.underscore.to_sym
end
##
# Indicates whether or not self registration should be limited for the provider
# with the given name.
#
# @param provider [String] Name of the provider
def self.limit_self_registration?(provider:)
Hash(find_provider_by_name(provider))[:limit_self_registration]
end
def self.warn_unavailable(name)
RequestStore.fetch("warn_unavailable_auth_#{name}") do
Rails.logger.warn { "OmniAuth SSO strategy #{name} is only available for Enterprise Editions." }
@@ -62,11 +62,11 @@ module OpenIDConnect
end
def create_params
params.require(:openid_connect_provider).permit(:name, :display_name, :identifier, :secret)
params.require(:openid_connect_provider).permit(:name, :display_name, :identifier, :secret, :limit_self_registration)
end
def update_params
params.require(:openid_connect_provider).permit(:display_name, :identifier, :secret)
params.require(:openid_connect_provider).permit(:display_name, :identifier, :secret, :limit_self_registration)
end
def find_provider
@@ -21,14 +21,27 @@ module OpenIDConnect
delegate :scope, to: :omniauth_provider, allow_nil: true
delegate :to_h, to: :omniauth_provider, allow_nil: false
##
# Controls whether or not self registration shall be limited for this provider.
#
# See also:
# - OpenProject::Plugins::AuthPlugin.limit_self_registration?
# - OpenProject::AuthPlugins::Patches::RegisterUserServicePatch
attr_reader :limit_self_registration
def initialize(omniauth_provider)
@omniauth_provider = omniauth_provider
@errors = ActiveModel::Errors.new(self)
@display_name = omniauth_provider.to_h[:display_name]
@limit_self_registration = initial_value_for_limit_self_registration
end
def self.initialize_with(params)
new(NewProvider.new(params))
do_limit = params[:limit_self_registration]
new(NewProvider.new(params.except(:limit_self_registration))).tap do |p|
p.limit_self_registration = String(do_limit).to_bool unless do_limit.nil?
end
end
def new_record?
@@ -39,6 +52,24 @@ module OpenIDConnect
omniauth_provider.is_a?(OmniAuth::OpenIDConnect::Provider)
end
def limit_self_registration?
@limit_self_registration
end
def limit_self_registration=(value)
@limit_self_registration = value
end
def to_h
return {} if omniauth_provider.nil?
omniauth_provider.to_h.merge(limit_self_registration: limit_self_registration?)
end
def limit_self_registration_default
name == "google" # limit by default only for Google since anyone can sign in
end
def id
return nil unless persisted?
@@ -46,33 +77,64 @@ module OpenIDConnect
end
def valid?
@errors.add(:name, :invalid) unless ALLOWED_TYPES.include?(name)
@errors.add(:name, :invalid) unless type_allowed?(name)
@errors.add(:identifier, :blank) if identifier.blank?
@errors.add(:secret, :blank) if secret.blank?
@errors.none?
end
##
# Checks if the provider with the given name is of an allowed type.
#
# Types can be followed by a period and arbitrary names to add several
# providers of the same type. E.g. 'azure', 'azure.dep1', 'azure.dep2'.
def type_allowed?(name)
ALLOWED_TYPES.any? { |allowed| name =~ /\A#{allowed}(\..+)?\Z/ }
end
def save
return false unless valid?
config = Setting.plugin_openproject_openid_connect || Hash.new
config["providers"] ||= Hash.new
config["providers"][name] = omniauth_provider.to_h.stringify_keys
Setting.plugin_openproject_openid_connect = config
Setting.plugin_openproject_openid_connect = setting_with_provider
true
end
def destroy
config = Setting.plugin_openproject_openid_connect
config["providers"] ||= {}
config["providers"].delete(name)
Setting.plugin_openproject_openid_connect = config
Setting.plugin_openproject_openid_connect = setting_without_provider
true
end
def setting_with_provider
setting.deep_merge "providers" => { name => to_h.stringify_keys }
end
def setting_without_provider
setting.tap do |s|
s["providers"].delete name
end
end
def setting
Hash(Setting.plugin_openproject_openid_connect).tap do |h|
h["providers"] ||= Hash.new
end
end
# https://api.rubyonrails.org/classes/ActiveModel/Errors.html
def read_attribute_for_validation(attr)
send(attr)
end
private
def initial_value_for_limit_self_registration
if omniauth_provider.configuration&.has_key? :limit_self_registration
omniauth_provider.configuration[:limit_self_registration]
else
limit_self_registration_default
end
end
end
end
@@ -22,4 +22,11 @@
<div class="form--field -required">
<%= f.text_field :secret, required: true, container_class: '-middle' %>
</div>
<div class="form--field">
<%= f.check_box :limit_self_registration, required: false, container_class: '-middle' %>
<div class="form--field-instructions">
<%= I18n.t('openid_connect.setting_instructions.limit_self_registration') %>
</div>
</div>
</fieldset>
@@ -10,6 +10,7 @@ de:
identifier: Identifier
secret: Secret
scope: Scope
limit_self_registration: Selbstregistrierung einschränken
openid_connect:
menu_title: OpenID-Provider
providers:
@@ -20,3 +21,7 @@ de:
singular: OpenID-Provider
upsale:
description: Use existing OpenID credentials with OpenProject for easier access and interoperability with a range of other providers.
setting_instructions:
limit_self_registration: >
Wenn diese Option aktiv ist, können sich neue Nutzer mit diesem OpenID-Provider nur registrieren,
wenn die Selbstregistrierungs-Einstellung es erlaubt.
@@ -10,6 +10,7 @@ en:
identifier: Identifier
secret: Secret
scope: Scope
limit_self_registration: Limit self registration
openid_connect:
menu_title: OpenID providers
providers:
@@ -18,3 +19,6 @@ en:
no_results_table: No providers have been defined yet.
plural: OpenID providers
singular: OpenID provider
setting_instructions:
limit_self_registration: >
If enabled users can only register using this provider if the self registration setting allows for it.
@@ -105,11 +105,40 @@ describe OpenIDConnect::ProvidersController do
end
describe '#create' do
it 'is successful if valid params' do
post :create, params: { openid_connect_provider: valid_params }
expect(flash[:notice]).to eq(I18n.t(:notice_successful_create))
expect(Setting.plugin_openproject_openid_connect["providers"]).to have_key("azure")
expect(response).to be_redirect
context 'with valid params' do
let(:params) { { openid_connect_provider: valid_params } }
before do
post :create, params:
end
it 'is successful' do
expect(flash[:notice]).to eq(I18n.t(:notice_successful_create))
expect(Setting.plugin_openproject_openid_connect["providers"]).to have_key("azure")
expect(response).to be_redirect
end
context 'with limit_self_registration checked' do
let(:params) do
{ openid_connect_provider: valid_params.merge(limit_self_registration: 1) }
end
it 'sets the setting' do
expect(OpenProject::Plugins::AuthPlugin)
.to be_limit_self_registration provider: valid_params[:name]
end
end
context 'with limit_self_registration unchecked' do
let(:params) do
{ openid_connect_provider: valid_params.merge(limit_self_registration: 0) }
end
it 'does not set the setting' do
expect(OpenProject::Plugins::AuthPlugin)
.not_to be_limit_self_registration provider: valid_params[:name]
end
end
end
it 'renders an error if invalid params' do
@@ -130,6 +159,39 @@ describe OpenIDConnect::ProvidersController do
expect(assigns[:provider]).to be_present
expect(response).to render_template 'edit'
end
context(
'with limit_self_registration set',
with_settings: {
plugin_openproject_openid_connect: {
"providers" => {
"azure" => {
"identifier" => "IDENTIFIER",
"secret" => "SECRET",
"limit_self_registration" => true
}
}
}
}
) do
before do
get :edit, params: { id: 'azure' }
end
it 'shows limit_self_registration as checked' do
expect(assigns[:provider]).to be_limit_self_registration
end
end
context 'with limit_self_registration not set' do
before do
get :edit, params: { id: 'azure' }
end
it 'shows limit_self_registration as unchecked' do
expect(assigns[:provider]).not_to be_limit_self_registration
end
end
end
context 'when not found' do
@@ -0,0 +1,96 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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'
describe OpenIDConnect::Provider do
let(:provider) do
described_class.initialize_with name: "azure", identifier: "id", secret: "secret"
end
def auth_plugin
OpenProject::Plugins::AuthPlugin
end
describe 'limit_self_registration' do
before do
# required so that the auth plugin sees any providers (ee feature)
allow(EnterpriseToken).to receive(:show_banners?).and_return false
end
context 'with no limited providers' do
it "shows the provider as unlimited" do
expect(auth_plugin).not_to be_limit_self_registration provider: provider.name
end
context 'when set to true' do
before do
provider.limit_self_registration = true
end
it "saving the provider makes it limited" do
provider.save
expect(auth_plugin).to be_limit_self_registration provider: provider.name
end
end
context 'when set to false' do
before do
provider.limit_self_registration = false
end
it "saving the provider does nothing" do
provider.save
expect(auth_plugin).not_to be_limit_self_registration provider: provider.name
end
end
end
context(
'with a limited provider',
with_settings: {
plugin_openproject_openid_connect: {
"providers" => {
"azure" => {
"name" => "azure",
"identifier" => "id",
"secret" => "secret",
"limit_self_registration" => true
}
}
}
}
) do
it "shows the provider as limited" do
expect(auth_plugin).to be_limit_self_registration provider: provider.name
end
end
end
end
@@ -76,6 +76,48 @@ describe Users::RegisterUserService do
end
end
describe '#register_omniauth_user' do
let(:user) { User.new(status: Principal.statuses[:registered], identity_url: 'azure:1234') }
let(:instance) { described_class.new(user) }
let(:call) { instance.call }
before do
allow(user).to receive(:activate)
allow(user).to receive(:save).and_return true
# required so that the azure provider is visible (ee feature)
allow(EnterpriseToken).to receive(:show_banners?).and_return false
with_all_registration_options do |_type|
call
end
end
it 'tries to activate that user regardless of settings' do
expect(call).to be_success
expect(call.result).to eq user
expect(call.message).to eq I18n.t(:notice_account_registered_and_logged_in)
end
context(
'with limit_self_registration enabled and self_registration disabled',
with_settings: {
self_registration: 0,
plugin_openproject_openid_connect: {
providers: {
azure: { identifier: "foo", secret: "bar", limit_self_registration: true }
}
}
}
) do
it 'fails to activate due to disabled self registration' do
expect(call).not_to be_success
expect(call.result).to eq user
expect(call.message).to eq I18n.t('account.error_self_registration_disabled')
end
end
end
describe '#ensure_registration_allowed!' do
it 'returns an error for disabled' do
allow(Setting).to receive(:self_registration).and_return(0)