├── .github
└── workflows
│ ├── 5_1_8.yml
│ ├── 6_0_5.yml
│ └── master.yml
├── Gemfile
├── LICENSE
├── README.md
├── app
├── overrides
│ └── account
│ │ └── login.rb
└── views
│ ├── redmine_omniauth_cas
│ └── _view_account_login_top.html.erb
│ └── settings
│ └── _omniauth_cas_settings.html.erb
├── assets
└── stylesheets
│ └── login.css
├── config
├── locales
│ ├── en.yml
│ ├── fr.yml
│ ├── pt-BR.yml
│ └── zh.yml
└── routes.rb
├── init.rb
├── initializers
└── redmine_omniauth_cas.rb
├── lib
├── omni_auth
│ ├── dynamic_full_host.rb
│ └── patches.rb
├── redmine_omniauth_cas.rb
└── redmine_omniauth_cas
│ ├── account_controller_patch.rb
│ ├── account_helper_patch.rb
│ ├── application_controller_patch.rb
│ └── hooks.rb
└── spec
├── controllers
└── account_controller_patch_spec.rb
├── helpers
└── account_helper_patch_spec.rb
├── integration
└── account_patch_spec.rb
└── models
└── redmine_omniauth_cas_spec.rb
/.github/workflows/5_1_8.yml:
--------------------------------------------------------------------------------
1 | name: Tests 5.1.8
2 |
3 | env:
4 | PLUGIN_NAME: redmine_omniauth_cas
5 | REDMINE_VERSION: 5.1.8
6 | RAILS_ENV: test
7 |
8 | on:
9 | push:
10 | pull_request:
11 |
12 | jobs:
13 | test:
14 | name: ${{ github.workflow }} ${{ matrix.db }} ruby-${{ matrix.ruby }}
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | ruby: ['3.2']
20 | db: ['postgres']
21 | fail-fast: false
22 |
23 | services:
24 | postgres:
25 | image: postgres:13
26 | env:
27 | POSTGRES_DB: redmine
28 | POSTGRES_USER: postgres
29 | POSTGRES_PASSWORD: postgres
30 | ports:
31 | - 5432:5432
32 | options: >-
33 | --health-cmd pg_isready
34 | --health-interval 10s
35 | --health-timeout 5s
36 | --health-retries 5
37 |
38 | steps:
39 | - name: Checkout Redmine
40 | uses: actions/checkout@v4
41 | with:
42 | repository: redmine/redmine
43 | ref: ${{ env.REDMINE_VERSION }}
44 | path: redmine
45 |
46 | - name: Update package archives
47 | run: sudo apt-get update --yes --quiet
48 |
49 | - name: Install package dependencies
50 | run: >
51 | sudo apt-get update && sudo apt-get install --yes --quiet
52 | build-essential
53 | cmake
54 | libicu-dev
55 | libpq-dev
56 | ghostscript
57 | gsfonts
58 |
59 | - name: Set up chromedriver
60 | uses: nanasess/setup-chromedriver@master
61 | - run: |
62 | export DISPLAY=:99
63 | chromedriver --url-base=/wd/hub &
64 | sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional
65 |
66 | - name: Allow imagemagick to read PDF files
67 | run: |
68 | echo '
4 | 5 | <%= check_box_tag 'settings[enabled]', true, @settings['enabled'] %> 6 |
7 |
8 |
9 | <%= text_field_tag 'settings[cas_server]', @settings['cas_server'], :size => 50 %>
10 |
11 | <%= l(:label_example) %>: https://cas.example.com
12 |
14 |
15 | <%= text_field_tag 'settings[cas_service_validate_url]', @settings['cas_service_validate_url'], :size => 50 %>
16 |
17 | <%= l(:label_example) %>: https://cas.example.net/serviceValidate
18 |
20 | 21 | <%= text_field_tag 'settings[label_login_with_cas]', @settings['label_login_with_cas'], :size => 50 %> 22 |
23 |24 | 25 | <%= check_box_tag 'settings[replace_redmine_login]', true, @settings['replace_redmine_login'] %> 26 |
27 | -------------------------------------------------------------------------------- /assets/stylesheets/login.css: -------------------------------------------------------------------------------- 1 | #cas-login { 2 | margin: 3em auto 0; 3 | text-align: center; 4 | } 5 | 6 | #cas-login form { 7 | display: inline-block; 8 | max-width: 800px; 9 | } 10 | 11 | #cas-login input[type="submit"] { 12 | padding-left: 1em; 13 | padding-right: 1em; 14 | -moz-border-radius: 20px; 15 | -webkit-border-radius: 20px; 16 | border-radius: 20px; 17 | border: none; 18 | color: #fff; 19 | background-color: #229e43; 20 | height: 32px; 21 | font-size: 16px; 22 | font-weight: bold; 23 | } 24 | 25 | #cas-login input[type="submit"]:hover { 26 | background-color: #20ad46; 27 | } 28 | 29 | #cas-login input[type="submit"]:active { 30 | position: relative; 31 | top: 1px; 32 | } 33 | 34 | #login-form table { 35 | margin-top: 2em; 36 | } 37 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | label_login_with_cas: Login with CAS 3 | label_login_page_text: Login page text 4 | label_cas_enabled: Enable CAS authentication 5 | label_cas_server: CAS server URL 6 | label_cas_service_validate_url: CAS validation URL (if different) 7 | label_replace_redmine_login: Replace Redmine login page 8 | text_full_logout_proposal: You may want to %{value} before trying an other username. 9 | text_logout_from_cas: logout from CAS 10 | error_cas_no_ticket: No CAS ticket was found during callback. Please try to authenticate again 11 | error_cas_invalid_ticket: An invalid CAS ticket was specified, it may have expired. Please try authenticating in again. 12 | error_cas_unknown: An unknown error occurred during CAS callback processing. 13 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | label_login_with_cas: S'authentifier avec CAS 3 | label_login_page_text: Texte de la page de login 4 | label_cas_enabled: Activer l'authentification CAS 5 | label_cas_server: Adresse du serveur CAS 6 | label_cas_service_validate_url: Adresse de validation CAS (si différente) 7 | label_replace_redmine_login: Remplacer la page de login de Redmine 8 | text_full_logout_proposal: Peut-être voulez-vous vous %{value} avant d'essayer un autre nom d'utilisateur ? 9 | text_logout_from_cas: déconnecter de CAS 10 | error_cas_no_ticket: Aucun ticket CAS trouvé. Vous devriez re-tenter une authentification 11 | error_cas_invalid_ticket: Un ticket CAS invalide a été spécifié, il a peut-être expiré. Veuillez réessayer de vous authentifier. 12 | error_cas_unknown: Une erreur inconnue s'est produite lors du retour d'authentification CAS. 13 | -------------------------------------------------------------------------------- /config/locales/pt-BR.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | label_login_with_cas: Login com CAS 3 | label_login_page_text: Texto na página de login 4 | label_cas_enabled: Habiltar autenticação com CAS 5 | label_cas_server: URL do servidor CAS 6 | label_cas_service_validate_url: URL de validação do CAS (se diferente) 7 | label_replace_redmine_login: Substituir página de login do Redmine 8 | text_full_logout_proposal: Você precisa acessar %{value} antes de tentar logar com outro usuário. 9 | text_logout_from_cas: logout do CAS 10 | error_cas_no_ticket: Ticket do CAS não recebido no callback. Tente autenticar novamente. 11 | error_cas_invalid_ticket: Ticket inválido retornado, ele pode estar expirado. Tente logar novamente. 12 | error_cas_unknown: Erro desconhecido ao processar o callback do CAS. 13 | -------------------------------------------------------------------------------- /config/locales/zh.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | label_login_with_cas: CAS 登录 3 | label_login_page_text: 登录页文本 4 | label_cas_enabled: 开启 CAS 登录 5 | label_cas_server: CAS 服务器地址 6 | label_cas_service_validate_url: CAS 验证地址 (如果不同) 7 | label_replace_redmine_login: 替换 Redmine 登录页 8 | text_full_logout_proposal: 在尝试其他用户名之前,您可能需要 %{value}。 9 | text_logout_from_cas: 从 CAS 登出 10 | error_cas_no_ticket: 回调期间未找到 CAS ticket。请尝试再次进行身份验证 11 | error_cas_invalid_ticket: 指定了无效的 CAS ticket,它可能已过期。请再次尝试身份验证。 12 | error_cas_unknown: CAS 回调处理过程中发生未知错误。 13 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | RedmineApp::Application.routes.draw do 2 | match 'auth/failure', :controller => 'account', :action => 'login_with_cas_failure', via: [:get, :post] 3 | match 'auth/:provider/callback', :controller => 'account', :action => 'login_with_cas_callback', via: [:get, :post] 4 | match 'auth/:provider', :controller => 'account', :action => 'login_with_cas_redirect', via: [:get, :post] 5 | end 6 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | initializers_dir = File.join(Rails.root, "config", "initializers") 4 | if Dir.glob(File.join(initializers_dir, "redmine_omniauth_cas.rb")).blank? 5 | $stderr.puts "Omniauth CAS Plugin: Missing initialization file config/initializers/redmine_omniauth_cas.rb. " \ 6 | "Please copy the provided file to the config/initializers/ directory.\n" \ 7 | "You can copy/paste this command:\n" \ 8 | "cp #{File.join(Rails.root, "plugins", "redmine_omniauth_cas")}/initializers/redmine_omniauth_cas.rb #{File.join(initializers_dir, "redmine_omniauth_cas.rb")}" 9 | exit 1 10 | end 11 | 12 | require 'redmine' 13 | require_relative 'lib/redmine_omniauth_cas' 14 | require_relative 'lib/redmine_omniauth_cas/hooks' 15 | require_relative 'lib/omni_auth/patches' 16 | require_relative 'lib/omni_auth/dynamic_full_host' 17 | 18 | # Plugin generic informations 19 | Redmine::Plugin.register :redmine_omniauth_cas do 20 | name 'Redmine Omniauth plugin' 21 | description 'This plugin adds Omniauth support to Redmine' 22 | author 'Jean-Baptiste BARTH' 23 | author_url 'mailto:jeanbaptiste.barth@gmail.com' 24 | url 'https://github.com/jbbarth/redmine_omniauth_cas' 25 | version '6.0.4' 26 | requires_redmine :version_or_higher => '4.0.0' 27 | requires_redmine_plugin :redmine_base_rspec, :version_or_higher => '0.0.3' if Rails.env.test? 28 | settings :default => { 'enabled' => 'true', 'label_login_with_cas' => '', 'cas_server' => '' }, 29 | :partial => 'settings/omniauth_cas_settings' 30 | end 31 | -------------------------------------------------------------------------------- /initializers/redmine_omniauth_cas.rb: -------------------------------------------------------------------------------- 1 | 2 | # OmniAuth CAS 3 | setup_app = Proc.new do |env| 4 | addr = RedmineOmniauthCas.cas_server 5 | cas_server = URI.parse(addr) 6 | if cas_server 7 | env['omniauth.strategy'].options.merge! :host => cas_server.host, 8 | :port => cas_server.port, 9 | :path => (cas_server.path != "/" ? cas_server.path : nil), 10 | :ssl => cas_server.scheme == "https" 11 | end 12 | validate = RedmineOmniauthCas.cas_service_validate_url 13 | if validate 14 | env['omniauth.strategy'].options.merge! :service_validate_url => validate 15 | end 16 | # Dirty, not happy with it, but as long as I can't reproduce the bug 17 | # users are blocked because of failing OpenSSL checks, while the cert 18 | # is actually good, so... 19 | # TODO: try to understand why cert verification fails 20 | # Maybe https://github.com/intridea/omniauth/issues/404 can help 21 | env['omniauth.strategy'].options.merge! :disable_ssl_verification => true 22 | end 23 | 24 | begin 25 | # tell Rails we use this middleware, with some default value just in case 26 | Rails.application.config.middleware.use OmniAuth::Builder do 27 | provider :cas, :host => "localhost", 28 | :port => "9292", 29 | :ssl => false, 30 | :setup => setup_app 31 | end 32 | rescue FrozenError 33 | # This can happen if there is a crash after Rails has 34 | # started booting but before we've added our middleware. 35 | # The middlewares array will only be frozen if an earlier error occurs 36 | Rails.logger.warn("Unable to add OmniAuth::Builder middleware as the middleware stack is frozen") 37 | puts "/!\\ Unable to add OmniAuth::Builder middleware as the middleware stack is frozen" 38 | end 39 | -------------------------------------------------------------------------------- /lib/omni_auth/dynamic_full_host.rb: -------------------------------------------------------------------------------- 1 | # configures public url for our application 2 | module OmniAuth::DynamicFullHost 3 | def self.full_host_url(url = nil) 4 | # unescapes url on-the-fly because it might be double-escaped in some environments 5 | #(it happens for me at work with 2 reverse-proxies in front of the app...) 6 | url = CGI.unescape(url) if url 7 | 8 | # if no url found, fall back to config/app_config.yml addresses 9 | if url.blank? 10 | url = Setting["host_name"] 11 | # else, parse it and remove both request_uri and query_string 12 | else 13 | uri = URI.parse(URI::Parser.new.escape(url)) # Encode to ensure we only have ASCII charaters in url 14 | url = "#{uri.scheme}://#{uri.host}" 15 | url << ":#{uri.port}" unless uri.default_port == uri.port 16 | end 17 | url 18 | end 19 | end 20 | 21 | OmniAuth.config.full_host = Proc.new do |env| 22 | OmniAuth::DynamicFullHost.full_host_url(env["rack.session"]["omniauth.origin"] || env["omniauth.origin"]) 23 | end 24 | -------------------------------------------------------------------------------- /lib/omni_auth/patches.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth/cas' 2 | 3 | module OmniAuth::Patches 4 | # patch to disable return_url to avoid polluting the service URL 5 | def return_url 6 | {} 7 | end 8 | end 9 | 10 | module OmniAuth 11 | module Strategies 12 | class CAS 13 | prepend OmniAuth::Patches 14 | 15 | # patch to accept path (subdir) in cas_host 16 | option :path, nil 17 | 18 | # patch to accept a different host for service_validate_url 19 | def service_validate_url_with_different_host(service_url, ticket) 20 | service_url = Addressable::URI.parse(service_url) 21 | service_url.query_values = service_url.query_values.tap { |qs| qs.delete('ticket') } 22 | 23 | validate_url = Addressable::URI.parse(@options.service_validate_url) 24 | 25 | if service_url.host.nil? || validate_url.host.nil? 26 | cas_url + append_params(@options.service_validate_url, { :service => service_url.to_s, :ticket => ticket }) 27 | else 28 | append_params(@options.service_validate_url, { :service => service_url.to_s, :ticket => ticket }) 29 | end 30 | end 31 | 32 | # alias_method_chain is deprecated in Rails 5: replaced with two alias_method 33 | # as a quick workaround. Using the 'prepend' method can generate an 34 | # 'stack level too deep' error in conjunction with other (non ported) plugins. 35 | # alias_method_chain :service_validate_url, :different_host 36 | alias_method :service_validate_url_without_different_host, :service_validate_url 37 | alias_method :service_validate_url, :service_validate_url_with_different_host 38 | 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/redmine_omniauth_cas.rb: -------------------------------------------------------------------------------- 1 | module RedmineOmniauthCas 2 | class << self 3 | def settings_hash 4 | Setting["plugin_redmine_omniauth_cas"] 5 | end 6 | 7 | def enabled? 8 | settings_hash["enabled"] 9 | end 10 | 11 | def cas_server 12 | settings_hash["cas_server"] 13 | end 14 | 15 | def cas_service_validate_url 16 | settings_hash["cas_service_validate_url"].presence || nil 17 | end 18 | 19 | def label_login_with_cas 20 | settings_hash["label_login_with_cas"] 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/redmine_omniauth_cas/account_controller_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'account_controller' 2 | 3 | module RedmineOmniauthCas 4 | module AccountControllerPatch 5 | def self.included(base) 6 | base.send(:include, InstanceMethods) 7 | base.class_eval do 8 | # alias_method_chain is deprecated in Rails 5: replaced with two alias_method 9 | # as a quick workaround. Using the 'prepend' method can generate an 10 | # 'stack level too deep' error in conjunction with other (non ported) plugins. 11 | # alias_method_chain :logout, :cas 12 | # alias_method_chain :login, :cas 13 | alias_method :login_without_cas, :login 14 | alias_method :login, :login_with_cas 15 | alias_method :logout_without_cas, :logout 16 | alias_method :logout, :logout_with_cas 17 | end 18 | end 19 | 20 | module InstanceMethods 21 | 22 | def login_with_cas 23 | #TODO: test 'replace_redmine_login' feature 24 | if cas_settings["enabled"] && cas_settings["replace_redmine_login"] 25 | redirect_to :controller => "account", :action => "login_with_cas_redirect", :provider => "cas", :origin => back_url 26 | else 27 | login_without_cas 28 | end 29 | end 30 | 31 | def login_with_cas_redirect 32 | render plain: "Not Found", status: 404 33 | end 34 | 35 | def login_with_cas_callback 36 | auth = request.env["omniauth.auth"] 37 | 38 | #add a hook where you could define auto-creation of users for instance 39 | call_hook(:controller_account_before_cas_login, { :params => params, :auth => auth, :cookies => cookies }) 40 | 41 | #user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) 42 | user = User.find_by_login(auth["uid"]) || User.find_by_mail(auth["uid"]) 43 | 44 | # taken from original AccountController 45 | # maybe it should be splitted in core 46 | if user.blank? 47 | logger.warn "Failed login for '#{auth[:uid]}' from #{request.remote_ip} at #{Time.now.utc}" 48 | error = l(:notice_account_invalid_credentials).sub(/\.$/, '') 49 | if cas_settings["cas_server"].present? 50 | link = self.class.helpers.link_to(l(:text_logout_from_cas), cas_logout_url, :target => "_blank") 51 | error << ". #{l(:text_full_logout_proposal, :value => link)}" 52 | end 53 | if cas_settings["replace_redmine_login"] 54 | render_error({:message => error.html_safe, :status => 403}) 55 | return false 56 | else 57 | flash[:error] = error 58 | redirect_to signin_url 59 | end 60 | else 61 | user.update_attribute(:last_login_on, Time.now) 62 | params[:back_url] = request.env["omniauth.origin"] unless request.env["omniauth.origin"].blank? 63 | successful_authentication(user) 64 | #cannot be set earlier, because sucessful_authentication() triggers reset_session() 65 | session[:logged_in_with_cas] = true 66 | end 67 | end 68 | 69 | def login_with_cas_failure 70 | error = params[:message] || 'unknown' 71 | error = 'error_cas_' + error 72 | if cas_settings["replace_redmine_login"] 73 | render_error({:message => error.to_sym, :status => 500}) 74 | return false 75 | else 76 | flash[:error] = l(error.to_sym) 77 | redirect_to signin_url 78 | end 79 | end 80 | 81 | def logout_with_cas 82 | if cas_settings["enabled"] && session[:logged_in_with_cas] 83 | logout_user 84 | redirect_to cas_logout_url(home_url) 85 | else 86 | logout_without_cas 87 | end 88 | end 89 | 90 | private 91 | def cas_settings 92 | RedmineOmniauthCas.settings_hash 93 | end 94 | 95 | def cas_logout_url(service = nil) 96 | logout_uri = URI.parse(cas_settings["cas_server"] + "/").merge("./logout") 97 | if !service.blank? 98 | logout_uri.query = "gateway=1&service=#{service}" 99 | end 100 | logout_uri.to_s 101 | end 102 | 103 | end 104 | end 105 | end 106 | 107 | unless AccountController.included_modules.include? RedmineOmniauthCas::AccountControllerPatch 108 | AccountController.send(:include, RedmineOmniauthCas::AccountControllerPatch) 109 | end 110 | 111 | class AccountController 112 | skip_before_action :verify_authenticity_token, only: [:login_with_cas_callback] 113 | end 114 | -------------------------------------------------------------------------------- /lib/redmine_omniauth_cas/account_helper_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'account_helper' 2 | 3 | module RedmineOmniauthCas::AccountHelperPatch 4 | def label_for_cas_login 5 | RedmineOmniauthCas.label_login_with_cas.presence || l(:label_login_with_cas) 6 | end 7 | end 8 | 9 | AccountHelper.prepend RedmineOmniauthCas::AccountHelperPatch 10 | ActionView::Base.prepend AccountHelper 11 | -------------------------------------------------------------------------------- /lib/redmine_omniauth_cas/application_controller_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'application_controller' 2 | 3 | module RedmineOmniauthCas 4 | module ApplicationControllerPatch 5 | extend ActiveSupport::Concern 6 | 7 | def require_login 8 | if !User.current.logged? 9 | # Extract only the basic url parameters on non-GET requests 10 | if request.get? 11 | url = request.original_url 12 | 13 | ## START PATCH 14 | url.gsub!('http:', Setting.protocol + ":") 15 | ## END PATCH 16 | 17 | else 18 | url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id]) 19 | end 20 | respond_to do |format| 21 | format.html { 22 | if request.xhr? 23 | head :unauthorized 24 | else 25 | redirect_to signin_path(:back_url => url) 26 | end 27 | } 28 | format.any(:atom, :pdf, :csv) { 29 | redirect_to signin_path(:back_url => url) 30 | } 31 | format.api { 32 | if (Setting.rest_api_enabled? && accept_api_auth?) || Redmine::VERSION.to_s < '4.1' 33 | head(:unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"') 34 | else 35 | head(:forbidden) 36 | end 37 | } 38 | format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } 39 | format.any { head :unauthorized } 40 | end 41 | return false 42 | end 43 | true 44 | end 45 | 46 | # Returns a validated URL string if back_url is a valid url for redirection, 47 | # otherwise false 48 | def validate_back_url(back_url) 49 | return false if back_url.blank? 50 | 51 | if CGI.unescape(back_url).include?('..') 52 | return false 53 | end 54 | 55 | begin 56 | uri = Addressable::URI.parse(back_url) 57 | ## PATCHED : ignore scheme HTTPS/HTTP and port so redirection works behind reverse proxies 58 | [:host].each do |component| 59 | if uri.send(component).present? && uri.send(component) != request.send(component) 60 | return false 61 | end 62 | end 63 | # Remove unnecessary components to convert the URL into a relative URL 64 | uri.omit!(:scheme, :authority) 65 | rescue Addressable::URI::InvalidURIError 66 | return false 67 | end 68 | 69 | path = uri.to_s 70 | # Ensure that the remaining URL starts with a slash, followed by a 71 | # non-slash character or the end 72 | unless %r{\A/([^/]|\z)}.match?(path) 73 | return false 74 | end 75 | 76 | if %r{/(login|account/register|account/lost_password)}.match?(path) 77 | return false 78 | end 79 | 80 | if relative_url_root.present? && !path.starts_with?(relative_url_root) 81 | return false 82 | end 83 | 84 | return path 85 | end 86 | 87 | end 88 | end 89 | 90 | ApplicationController.prepend RedmineOmniauthCas::ApplicationControllerPatch 91 | -------------------------------------------------------------------------------- /lib/redmine_omniauth_cas/hooks.rb: -------------------------------------------------------------------------------- 1 | module RedmineOmniauthCas 2 | class Hooks < Redmine::Hook::ViewListener 3 | render_on :view_account_login_top, :partial => 'redmine_omniauth_cas/view_account_login_top' 4 | end 5 | 6 | class ModelHook < Redmine::Hook::Listener 7 | def after_plugins_loaded(_context = {}) 8 | require_relative 'account_controller_patch' 9 | require_relative 'account_helper_patch' 10 | require_relative 'application_controller_patch' 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/controllers/account_controller_patch_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe AccountController, type: :controller do 4 | render_views 5 | fixtures :users, :roles 6 | 7 | context "GET /login CAS button" do 8 | it "should show up only if there's a plugin setting for CAS URL" do 9 | Setting["plugin_redmine_omniauth_cas"]["cas_server"] = "" 10 | get :login 11 | assert_select '#cas-login', 0 12 | Setting["plugin_redmine_omniauth_cas"]["cas_server"] = "blah" 13 | get :login 14 | assert_select '#cas-login' 15 | end 16 | 17 | it "should correct double-escaped URL" do 18 | #I don't really know where this bug comes from but it seems URLs are escaped twice 19 | #in my setup which causes the back_url to be invalid. Let's try to be smart about 20 | #this directly in the plugin 21 | Setting["plugin_redmine_omniauth_cas"]["cas_server"] = "blah" 22 | get :login, params: {:back_url => "https%3A%2F%2Fblah%2F"} 23 | assert_select '#cas-login > form[action=?]', '/auth/cas?origin=https%3A%2F%2Fblah%2F' 24 | end 25 | end 26 | 27 | context "GET login_with_cas_callback" do 28 | it "should redirect to /my/page after successful login" do 29 | request.env["omniauth.auth"] = {"uid" => "admin"} 30 | get :login_with_cas_callback, params: {:provider => "cas"} 31 | expect(response).to redirect_to('/my/page') 32 | end 33 | 34 | it "should redirect to /login after failed login" do 35 | request.env["omniauth.auth"] = {"uid" => "non-existent"} 36 | Setting["plugin_redmine_omniauth_cas"]["cas_server"] = "http://cas.server/" 37 | get :login_with_cas_callback, params: {:provider => "cas"} 38 | expect(response).to redirect_to('/login') 39 | end 40 | 41 | it "should set a boolean in session to keep track of login" do 42 | request.env["omniauth.auth"] = {"uid" => "admin"} 43 | get :login_with_cas_callback, params: {:provider => "cas"} 44 | expect(response).to redirect_to('/my/page') 45 | assert session[:logged_in_with_cas] 46 | end 47 | 48 | it "should redirect to Home if not logged in with CAS" do 49 | get :logout 50 | expect(response).to redirect_to(home_url) 51 | end 52 | 53 | it "should redirect to CAS logout if previously logged in with CAS" do 54 | session[:logged_in_with_cas] = true 55 | Setting["plugin_redmine_omniauth_cas"]["cas_server"] = "http://cas.server" 56 | get :logout 57 | expect(response).to redirect_to('http://cas.server/logout?gateway=1&service=http://test.host/') 58 | end 59 | 60 | it "should respect path in CAS server when generating logout url" do 61 | session[:logged_in_with_cas] = true 62 | Setting["plugin_redmine_omniauth_cas"]["cas_server"] = "http://cas.server/cas" 63 | get :logout 64 | expect(response).to redirect_to('http://cas.server/cas/logout?gateway=1&service=http://test.host/') 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/helpers/account_helper_patch_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe AccountHelper do 4 | include Redmine::I18n 5 | 6 | context "#label_for_cas_login" do 7 | it "should use label_login_with_cas plugin setting if not blank" do 8 | label = "Log in with SSO" 9 | Setting["plugin_redmine_omniauth_cas"]["label_login_with_cas"] = label 10 | expect(label_for_cas_login).to eq label 11 | end 12 | 13 | it "should default to localized :label_login_with_cas if no setting present" do 14 | Setting["plugin_redmine_omniauth_cas"]["label_login_with_cas"] = nil 15 | expect(label_for_cas_login).to eq l(:label_login_with_cas) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/integration/account_patch_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "AccountPatch", :type => :request do 4 | fixtures :users, :roles, :email_addresses 5 | 6 | context "GET /auth/:provider" do 7 | it "should route to a blank action (intercepted by omniauth middleware)" do 8 | assert_routing( 9 | { :method => :get, :path => "/auth/blah" }, 10 | { :controller => 'account', :action => 'login_with_cas_redirect', :provider => 'blah' } 11 | ) 12 | end 13 | # TODO: some real test? 14 | end 15 | context "GET /auth/:provider/callback" do 16 | it "should route things correctly" do 17 | assert_routing( 18 | { :method => :get, :path => "/auth/blah/callback" }, 19 | { :controller => 'account', :action => 'login_with_cas_callback', :provider => 'blah' } 20 | ) 21 | end 22 | 23 | context "OmniAuth CAS strategy" do 24 | before do 25 | Setting.default_language = 'en' 26 | OmniAuth.config.test_mode = true 27 | end 28 | 29 | it "should authorize login if user exists with this login" do 30 | OmniAuth.config.mock_auth[:cas] = OmniAuth::AuthHash.new({ 'uid' => 'admin' }) 31 | Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:cas] 32 | get '/auth/cas/callback' 33 | expect(response).to redirect_to('/my/page') 34 | get '/my/page' 35 | expect(response.body).to match /Logged in as.*admin/im 36 | end 37 | 38 | it "should authorize login if user exists with this email" do 39 | OmniAuth.config.mock_auth[:cas] = OmniAuth::AuthHash.new({ 'uid' => 'admin@somenet.foo' }) 40 | Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:cas] 41 | get '/auth/cas/callback' 42 | expect(response).to redirect_to('/my/page') 43 | get '/my/page' 44 | expect(response.body).to match /Logged in as.*admin/im 45 | end 46 | 47 | it "should update last_login_on field" do 48 | user = User.find(1) 49 | user.update_attribute(:last_login_on, Time.now - 6.hours) 50 | OmniAuth.config.mock_auth[:cas] = OmniAuth::AuthHash.new({ 'uid' => 'admin' }) 51 | Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:cas] 52 | get '/auth/cas/callback' 53 | expect(response).to redirect_to('/my/page') 54 | user.reload 55 | assert Time.now - user.last_login_on < 30.seconds 56 | end 57 | 58 | it "should refuse login if user doesn't exist" do 59 | OmniAuth.config.mock_auth[:cas] = OmniAuth::AuthHash.new({ 'uid' => 'johndoe' }) 60 | Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:cas] 61 | get '/auth/cas/callback' 62 | expect(response).to redirect_to('/login') 63 | follow_redirect! 64 | expect(User.current).to eq User.anonymous 65 | assert_select 'div.flash.error', :text => /Invalid user or password/ 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/models/redmine_omniauth_cas_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "RedmineOmniAuthCAS" do 4 | context "#cas_service_validate_url" do 5 | it "should return setting if not blank" do 6 | url = "cas.example.com/validate" 7 | Setting["plugin_redmine_omniauth_cas"]["cas_service_validate_url"] = url 8 | expect(RedmineOmniauthCas.cas_service_validate_url).to eq url 9 | end 10 | 11 | it "should return nil if setting is blank" do 12 | Setting["plugin_redmine_omniauth_cas"]["cas_service_validate_url"] = "" 13 | expect(RedmineOmniauthCas.cas_service_validate_url).to be_nil 14 | end 15 | end 16 | 17 | context "dynamic full host" do 18 | it "should return host name from setting if no url" do 19 | Setting["host_name"] = "http://redmine.example.com" 20 | expect(OmniAuth::DynamicFullHost.full_host_url).to eq "http://redmine.example.com" 21 | end 22 | 23 | it "should return host name from url if url is present" do 24 | url = "https://redmine.example.com:3000/some/path" 25 | expect(OmniAuth::DynamicFullHost.full_host_url(url)).to eq "https://redmine.example.com:3000" 26 | end 27 | end 28 | end 29 | --------------------------------------------------------------------------------