From c8b8a0cb75a8d577c59464868563a8ceb8641a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karel=20Pi=C4=8Dman?= Date: Wed, 27 Nov 2024 12:18:55 +0100 Subject: [PATCH] Setting module --- .rubocop.yml | 11 +- app/controllers/redmine_oauth_controller.rb | 84 +++---- .../_view_account_login_bottom.html.erb | 16 +- init.rb | 6 +- lib/redmine_oauth.rb | 222 ++++++++++++++++++ .../controllers/account_controller_hooks.rb | 2 +- .../hooks/views/login_view_hooks.rb | 3 +- lib/redmine_oauth/imap.rb | 8 +- .../patches/account_controller_patch.rb | 14 +- 9 files changed, 290 insertions(+), 76 deletions(-) create mode 100644 lib/redmine_oauth.rb diff --git a/.rubocop.yml b/.rubocop.yml index c9c9b3a..6184cb4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -33,6 +33,9 @@ require: - rubocop-rails # Rules for OAuth +Metrics/AbcSize: + Enabled: false + Metrics/BlockLength: Enabled: false @@ -42,16 +45,16 @@ Metrics/BlockNesting: Metrics/ClassLength: Enabled: false -Metrics/MethodLength: +Metrics/CyclomaticComplexity: Enabled: false -Metrics/AbcSize: +Metrics/ModuleLength: Enabled: false -Metrics/PerceivedComplexity: +Metrics/MethodLength: Enabled: false -Metrics/CyclomaticComplexity: +Metrics/PerceivedComplexity: Enabled: false Style/ExpandPathArguments: diff --git a/app/controllers/redmine_oauth_controller.rb b/app/controllers/redmine_oauth_controller.rb index 5d10cbe..f70e402 100644 --- a/app/controllers/redmine_oauth_controller.rb +++ b/app/controllers/redmine_oauth_controller.rb @@ -31,7 +31,7 @@ def oauth session[:oauth_autologin] = params[:oauth_autologin] oauth_csrf_token = generate_csrf_token session[:oauth_csrf_token] = oauth_csrf_token - case Setting.plugin_redmine_oauth[:oauth_name] + case RedmineOauth.oauth_name when 'Azure AD' redirect_to oauth_client.auth_code.authorize_url( redirect_uri: oauth_callback_url, @@ -66,7 +66,7 @@ def oauth redirect_to oauth_client.auth_code.authorize_url( redirect_uri: oauth_callback_url, state: oauth_csrf_token, - scope: Setting.plugin_redmine_oauth[:custom_scope] + scope: RedmineOauth.custom_scope ) else flash['error'] = l(:oauth_invalid_provider) @@ -84,7 +84,7 @@ def oauth_callback raise StandardError, l(:notice_account_invalid_credentials) end - case Setting.plugin_redmine_oauth[:oauth_name] + case RedmineOauth.oauth_name when 'Azure AD' token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url) user_info = JWT.decode(token.token, nil, false).first @@ -110,7 +110,7 @@ def oauth_callback when 'Okta' token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url) userinfo_response = token.get( - "/oauth2/#{Setting.plugin_redmine_oauth[:tenant_id]}/v1/userinfo", + "/oauth2/#{RedmineOauth.tenant_id}/v1/userinfo", headers: { 'Accept' => 'application/json' } ) user_info = JSON.parse(userinfo_response.body) @@ -118,24 +118,24 @@ def oauth_callback email = user_info['email'] when 'Custom' token = oauth_client.auth_code.get_token(params['code'], redirect_uri: oauth_callback_url) - if Setting.plugin_redmine_oauth[:custom_profile_endpoint].strip.empty? + if RedmineOauth.custom_profile_endpoint.empty? user_info = JWT.decode(token.token, nil, false).first else userinfo_response = token.get( - Setting.plugin_redmine_oauth[:custom_profile_endpoint], + RedmineOauth.custom_profile_endpoint, headers: { 'Accept' => 'application/json' } ) user_info = JSON.parse(userinfo_response.body) end - user_info['login'] = user_info[Setting.plugin_redmine_oauth[:custom_uid_field]] - email = user_info[Setting.plugin_redmine_oauth[:custom_email_field]] + user_info['login'] = user_info[RedmineOauth.custom_uid_field] + email = user_info[RedmineOauth.custom_email_field] else raise StandardError, l(:oauth_invalid_provider) end raise StandardError, l(:oauth_no_verified_email) unless email # Roles - keys = Setting.plugin_redmine_oauth[:validate_user_roles]&.split('.') + keys = RedmineOauth.validate_user_roles.split('.') if keys&.size&.positive? roles = user_info while keys.size.positive? @@ -198,7 +198,7 @@ def try_to_login(email, info) elsif user.active? # Active handle_active_user user user.update_last_login_on! - if Setting.plugin_redmine_oauth[:update_login] && (info['login'] || info['unique_name']) + if RedmineOauth.update_login && (info['login'] || info['unique_name']) user.login = info['login'] || info['unique_name'] Rails.logger.error(user.errors.full_messages.to_sentence) unless user.save end @@ -209,17 +209,15 @@ def try_to_login(email, info) else # Locked handle_inactive_user user end - elsif Setting.plugin_redmine_oauth[:self_registration] && Setting.plugin_redmine_oauth[:self_registration] != '0' + elsif RedmineOauth.self_registration.positive? # Create on the fly user = User.new user.mail = email firstname, lastname = info['name'].split if info['name'].present? - key = Setting.plugin_redmine_oauth[:custom_firstname_field] - key ||= 'given_name' + key = RedmineOauth.custom_firstname_field firstname ||= info[key] user.firstname = firstname - key = Setting.plugin_redmine_oauth[:custom_lastname_field] - key ||= 'family_name' + key = RedmineOauth.custom_lastname_field lastname ||= info[key] user.lastname = lastname user.mail = email @@ -228,12 +226,12 @@ def try_to_login(email, info) user.login = login user.random_password user.register - case Setting.plugin_redmine_oauth[:self_registration] - when '1' + case RedmineOauth.self_registration + when 1 register_by_email_activation(user) do onthefly_creation_failed user end - when '3' + when 3 register_automatically(user) do onthefly_creation_failed user end @@ -256,63 +254,59 @@ def try_to_login(email, info) def oauth_client return @client if @client - site = Setting.plugin_redmine_oauth[:site]&.chomp('/') + site = RedmineOauth.site raise StandardError, l(:oauth_invalid_provider) unless site @client = - case Setting.plugin_redmine_oauth[:oauth_name] + case RedmineOauth.oauth_name when 'Azure AD' - url = if Setting.plugin_redmine_oauth[:oauth_version].present? - "#{Setting.plugin_redmine_oauth[:oauth_version]}/" - else - '' - end + url = RedmineOauth.oauth_version.present? ? "#{RedmineOauth.oauth_version}/" : '' OAuth2::Client.new( - Setting.plugin_redmine_oauth[:client_id], - Redmine::Ciphering.decrypt_text(Setting.plugin_redmine_oauth[:client_secret]), + RedmineOauth.client_id, + Redmine::Ciphering.decrypt_text(RedmineOauth.client_secret), site: site, - authorize_url: "/#{Setting.plugin_redmine_oauth[:tenant_id]}/oauth2/#{url}authorize", - token_url: "/#{Setting.plugin_redmine_oauth[:tenant_id]}/oauth2/#{url}token" + authorize_url: "/#{RedmineOauth.tenant_id}/oauth2/#{url}authorize", + token_url: "/#{RedmineOauth.tenant_id}/oauth2/#{url}token" ) when 'GitLab' OAuth2::Client.new( - Setting.plugin_redmine_oauth[:client_id], - Redmine::Ciphering.decrypt_text(Setting.plugin_redmine_oauth[:client_secret]), + RedmineOauth.client_id, + Redmine::Ciphering.decrypt_text(RedmineOauth.client_secret), site: site, authorize_url: '/oauth/authorize', token_url: '/oauth/token' ) when 'Google' OAuth2::Client.new( - Setting.plugin_redmine_oauth[:client_id], - Redmine::Ciphering.decrypt_text(Setting.plugin_redmine_oauth[:client_secret]), + RedmineOauth.client_id, + Redmine::Ciphering.decrypt_text(RedmineOauth.client_secret), site: site, authorize_url: '/o/oauth2/v2/auth', token_url: 'https://oauth2.googleapis.com/token' ) when 'Keycloak' OAuth2::Client.new( - Setting.plugin_redmine_oauth[:client_id], - Redmine::Ciphering.decrypt_text(Setting.plugin_redmine_oauth[:client_secret]), + RedmineOauth.client_id, + Redmine::Ciphering.decrypt_text(RedmineOauth.client_secret), site: site, - authorize_url: "/realms/#{Setting.plugin_redmine_oauth[:tenant_id]}/protocol/openid-connect/auth", - token_url: "/realms/#{Setting.plugin_redmine_oauth[:tenant_id]}/protocol/openid-connect/token" + authorize_url: "/realms/#{RedmineOauth.tenant_id}/protocol/openid-connect/auth", + token_url: "/realms/#{RedmineOauth.tenant_id}/protocol/openid-connect/token" ) when 'Okta' OAuth2::Client.new( - Setting.plugin_redmine_oauth[:client_id], - Redmine::Ciphering.decrypt_text(Setting.plugin_redmine_oauth[:client_secret]), + RedmineOauth.client_id, + Redmine::Ciphering.decrypt_text(RedmineOauth.client_secret), site: site, - authorize_url: "/oauth2/#{Setting.plugin_redmine_oauth[:tenant_id]}/v1/authorize", - token_url: "/oauth2/#{Setting.plugin_redmine_oauth[:tenant_id]}/v1/token" + authorize_url: "/oauth2/#{RedmineOauth.tenant_id}/v1/authorize", + token_url: "/oauth2/#{RedmineOauth.tenant_id}/v1/token" ) when 'Custom' OAuth2::Client.new( - Setting.plugin_redmine_oauth[:client_id], - Redmine::Ciphering.decrypt_text(Setting.plugin_redmine_oauth[:client_secret]), + RedmineOauth.client_id, + Redmine::Ciphering.decrypt_text(RedmineOauth.client_secret), site: site, - authorize_url: Setting.plugin_redmine_oauth[:custom_auth_endpoint], - token_url: Setting.plugin_redmine_oauth[:custom_token_endpoint] + authorize_url: RedmineOauth.custom_auth_endpoint, + token_url: RedmineOauth.custom_token_endpoint ) else raise StandardError, l(:oauth_invalid_provider) diff --git a/app/views/hooks/redmine_oauth/_view_account_login_bottom.html.erb b/app/views/hooks/redmine_oauth/_view_account_login_bottom.html.erb index 89f7671..103249c 100644 --- a/app/views/hooks/redmine_oauth/_view_account_login_bottom.html.erb +++ b/app/views/hooks/redmine_oauth/_view_account_login_bottom.html.erb @@ -20,9 +20,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. %> -<% if Setting.plugin_redmine_oauth[:button_icon] != 'none' %> +<% if RedmineOauth.button_icon != 'none' %> <%= stylesheet_link_tag 'redmine_oauth', plugin: 'redmine_oauth' %> - <% if Setting.plugin_redmine_oauth[:hide_login_form] %> + <% if RedmineOauth.hide_login_form %> <%= javascript_include_tag('redmine_oauth.js', plugin: :redmine_oauth) %>
<%= l(:button_login) %> @@ -32,17 +32,17 @@ <%= form_tag(oauth_path(back_url: back_url), method: :get, id: 'oauth-login') do %> <%= back_url_hidden_field_tag %> <%= button_tag(name: 'login-oauth', tabindex: 7, id: 'login-oauth-submit', title: l(:oauth_login_with), - style: "background: #{Setting.plugin_redmine_oauth[:button_color]}") do %> - + style: "background: #{RedmineOauth.button_color}") do %> + <%= l(:oauth_login_via, - oauth: Setting.plugin_redmine_oauth[:custom_name].blank? ? Setting.plugin_redmine_oauth[:oauth_name] : Setting.plugin_redmine_oauth[:custom_name]).html_safe %> + oauth: RedmineOauth.custom_name.blank? ? RedmineOauth.oauth_name : RedmineOauth.custom_name).html_safe %> <% end %> - <% if Setting.plugin_redmine_oauth[:oauth_login] %> + <% if RedmineOauth.oauth_login %>
<% end %> <% end %> @@ -69,7 +69,7 @@ } }); <%# Hidden login form %> - <% if Setting.plugin_redmine_oauth[:hide_login_form] %> + <% if RedmineOauth.hide_login_form %> let login_form = $('div#login-form'); login_form.appendTo('#oauth-fieldset-login-form'); login_form.toggle(); diff --git a/init.rb b/init.rb index f8581c6..106904c 100644 --- a/init.rb +++ b/init.rb @@ -19,11 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'redmine' -require File.expand_path('lib/redmine_oauth/hooks/controllers/account_controller_hooks', __dir__) -require File.expand_path('lib/redmine_oauth/hooks/views/base_view_hooks', __dir__) -require File.expand_path('lib/redmine_oauth/hooks/views/login_view_hooks', __dir__) -require File.expand_path('lib/redmine_oauth/patches/settings_controller_patch', __dir__) -require File.expand_path('lib/redmine_oauth/patches/account_controller_patch', __dir__) +require "#{File.dirname(__FILE__)}/lib/redmine_oauth" Redmine::Plugin.register :redmine_oauth do name 'Redmine OAuth plugin' diff --git a/lib/redmine_oauth.rb b/lib/redmine_oauth.rb new file mode 100644 index 0000000..3db1a1d --- /dev/null +++ b/lib/redmine_oauth.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +# Redmine plugin OAuth +# +# Karel Pičman +# +# 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. + +# Main module +module RedmineOauth + # Settings + class << self + def oauth_name + if Setting.plugin_redmine_oauth['oauth_name'].present? + Setting.plugin_redmine_oauth['oauth_name'].strip + else + 'none' + end + end + + def site + if Setting.plugin_redmine_oauth['site'].present? + Setting.plugin_redmine_oauth['site'].strip.chomp('/') + else + '' + end + end + + def client_id + if Setting.plugin_redmine_oauth['client_id'].present? + Setting.plugin_redmine_oauth['client_id'].strip + else + '' + end + end + + def client_secret + if Setting.plugin_redmine_oauth['client_secret'].present? + Setting.plugin_redmine_oauth['client_secret'].strip + else + '' + end + end + + def tenant_id + if Setting.plugin_redmine_oauth['tenant_id'].present? + Setting.plugin_redmine_oauth['tenant_id'].strip + else + '' + end + end + + def custom_name + if Setting.plugin_redmine_oauth['custom_name'].present? + Setting.plugin_redmine_oauth['custom_name'].strip + else + '' + end + end + + def custom_auth_endpoint + if Setting.plugin_redmine_oauth['custom_auth_endpoint'].present? + Setting.plugin_redmine_oauth['custom_auth_endpoint'].strip + else + '' + end + end + + def custom_token_endpoint + if Setting.plugin_redmine_oauth['custom_token_endpoint'].present? + Setting.plugin_redmine_oauth['custom_token_endpoint'].strip + else + '' + end + end + + def custom_profile_endpoint + if Setting.plugin_redmine_oauth['custom_profile_endpoint'].present? + Setting.plugin_redmine_oauth['custom_profile_endpoint'].strip + else + '' + end + end + + def custom_scope + if Setting.plugin_redmine_oauth['custom_scope'].present? + Setting.plugin_redmine_oauth['custom_scope'].strip + else + '' + end + end + + def preferred_username + if Setting.plugin_redmine_oauth['preferred_username'].present? + Setting.plugin_redmine_oauth['preferred_username'].strip + else + '' + end + end + + def email + if Setting.plugin_redmine_oauth['email'].present? + Setting.plugin_redmine_oauth['email'].strip + else + '' + end + end + + def button_color + if Setting.plugin_redmine_oauth['button_color'].present? + Setting.plugin_redmine_oauth['button_color'].strip + else + '#ffbe6f' + end + end + + def button_icon + if Setting.plugin_redmine_oauth['button_icon'].present? + Setting.plugin_redmine_oauth['button_icon'].strip + else + 'fas fa-address-card' + end + end + + def hide_login_form + if Setting.plugin_redmine_oauth['hide_login_form'].is_a?(TrueClass) + Setting.plugin_redmine_oauth['hide_login_form'] + else + Setting.plugin_redmine_oauth['hide_login_form'].to_i.positive? + end + end + + def self_registration + Setting.plugin_redmine_oauth['self_registration'].to_i + end + + def custom_firstname_field + if Setting.plugin_redmine_oauth['custom_firstname_field'].present? + Setting.plugin_redmine_oauth['custom_firstname_field'].strip + else + 'given_name' + end + end + + def custom_lastname_field + if Setting.plugin_redmine_oauth['custom_lastname_field'].present? + Setting.plugin_redmine_oauth['custom_lastname_field'].strip + else + 'family_name' + end + end + + def update_login + if Setting.plugin_redmine_oauth['update_login'].is_a?(TrueClass) + Setting.plugin_redmine_oauth['update_login'] + else + Setting.plugin_redmine_oauth['update_login'].to_i.positive? + end + end + + def oauth_logout + if Setting.plugin_redmine_oauth['oauth_logout'].is_a?(TrueClass) + Setting.plugin_redmine_oauth['oauth_logout'] + else + Setting.plugin_redmine_oauth['oauth_logout'].to_i.positive? + end + end + + def oauth_login + if Setting.plugin_redmine_oauth['oauth_login'].is_a?(TrueClass) + Setting.plugin_redmine_oauth['oauth_login'] + else + Setting.plugin_redmine_oauth['oauth_login'].to_i.positive? + end + end + + def custom_logout_endpoint + if Setting.plugin_redmine_oauth['custom_logout_endpoint'].present? + Setting.plugin_redmine_oauth['custom_logout_endpoint'].strip + else + '' + end + end + + def validate_user_roles + if Setting.plugin_redmine_oauth['validate_user_roles'].present? + Setting.plugin_redmine_oauth['validate_user_roles'].strip + else + '' + end + end + + def oauth_version + if Setting.plugin_redmine_oauth['oauth_version'].present? + Setting.plugin_redmine_oauth['oauth_version'].strip + else + '' + end + end + end +end + +# Hooks +require File.expand_path('redmine_oauth/hooks/controllers/account_controller_hooks', __dir__) +require File.expand_path('redmine_oauth/hooks/views/base_view_hooks', __dir__) +require File.expand_path('redmine_oauth/hooks/views/login_view_hooks', __dir__) + +# Patches +require File.expand_path('redmine_oauth/patches/settings_controller_patch', __dir__) +require File.expand_path('redmine_oauth/patches/account_controller_patch', __dir__) diff --git a/lib/redmine_oauth/hooks/controllers/account_controller_hooks.rb b/lib/redmine_oauth/hooks/controllers/account_controller_hooks.rb index 36cf6be..522a559 100644 --- a/lib/redmine_oauth/hooks/controllers/account_controller_hooks.rb +++ b/lib/redmine_oauth/hooks/controllers/account_controller_hooks.rb @@ -24,7 +24,7 @@ module Controllers # AccountController hooks class AccountControllerHooks < Redmine::Hook::Listener def controller_account_success_authentication_after(context = {}) - return unless Setting.plugin_redmine_oauth[:oauth_login] && context[:controller].params[:oauth_autologin] + return unless RedmineOauth.oauth_login && context[:controller].params[:oauth_autologin] context[:controller].set_oauth_autologin_cookie end diff --git a/lib/redmine_oauth/hooks/views/login_view_hooks.rb b/lib/redmine_oauth/hooks/views/login_view_hooks.rb index c7cf39d..ac3a52e 100644 --- a/lib/redmine_oauth/hooks/views/login_view_hooks.rb +++ b/lib/redmine_oauth/hooks/views/login_view_hooks.rb @@ -24,8 +24,7 @@ module Views # Login view hooks class LoginViewHooks < Redmine::Hook::ViewListener def view_account_login_bottom(context = {}) - oauth = Setting.plugin_redmine_oauth[:oauth_name] - return unless oauth.present? && (oauth != 'none') + return if RedmineOauth.oauth_name == 'none' context[:controller].send( :render_to_string, { partial: 'hooks/redmine_oauth/view_account_login_bottom', locals: context } diff --git a/lib/redmine_oauth/imap.rb b/lib/redmine_oauth/imap.rb index a774b47..6dabd8c 100644 --- a/lib/redmine_oauth/imap.rb +++ b/lib/redmine_oauth/imap.rb @@ -26,10 +26,10 @@ module IMAP class << self def check(imap_options = {}, options = {}) client = OAuth2::Client.new( - Setting.plugin_redmine_oauth[:client_id], - Redmine::Ciphering.decrypt_text(Setting.plugin_redmine_oauth[:client_secret]), - site: Setting.plugin_redmine_oauth[:site]&.chomp('/'), - token_url: "/#{Setting.plugin_redmine_oauth[:tenant_id]}/oauth2/v2.0/token" + RedmineOauth.client_id, + Redmine::Ciphering.decrypt_text(RedmineOauth.client_secret), + site: RedmineOauth.site, + token_url: "/#{RedmineOauth.tenant_id}/oauth2/v2.0/token" ) params = { scope: imap_options[:scope], diff --git a/lib/redmine_oauth/patches/account_controller_patch.rb b/lib/redmine_oauth/patches/account_controller_patch.rb index 529519a..d3d8ee8 100644 --- a/lib/redmine_oauth/patches/account_controller_patch.rb +++ b/lib/redmine_oauth/patches/account_controller_patch.rb @@ -33,22 +33,22 @@ def login def logout cookies.delete :oauth_autologin - return super if User.current.anonymous? || !request.post? || - Setting.plugin_redmine_oauth[:oauth_logout].blank? || session[:oauth_login].blank? + return super if User.current.anonymous? || !request.post? || !RedmineOauth.oauth_logout || + session[:oauth_login].blank? session.delete :oauth_login - site = Setting.plugin_redmine_oauth[:site]&.chomp('/') - id = Setting.plugin_redmine_oauth[:client_id] + site = RedmineOauth.site + id = RedmineOauth.client_id url = signout_url - case Setting.plugin_redmine_oauth[:oauth_name] + case RedmineOauth.oauth_name when 'Azure AD' logout_user redirect_to "#{site}/#{id}/oauth2/logout?post_logout_redirect_uri=#{url}" when 'Custom' logout_user - redirect_to Setting.plugin_redmine_oauth[:custom_logout_endpoint] + redirect_to RedmineOauth.custom_logout_endpoint when 'GitLab', 'Google' - Rails.logger.info "#{Setting.plugin_redmine_oauth[:oauth_name]} logout not implement" + Rails.logger.info "#{RedmineOauth.oauth_name} logout not implement" super when 'Keycloak' logout_user