diff --git a/Gemfile b/Gemfile index 65a3230..3de85a0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,2 @@ source 'https://rubygems.org' -gem 'httparty', '~> 0.14.0' +gem 'httparty', '~> 0.21.0' diff --git a/app/models/oic_session.rb b/app/models/oic_session.rb index cc11713..c6b6faf 100644 --- a/app/models/oic_session.rb +++ b/app/models/oic_session.rb @@ -1,4 +1,6 @@ class OicSession < ActiveRecord::Base + unloadable if self.respond_to?(:unloadable) + before_create :randomize_state! before_create :randomize_nonce! @@ -42,9 +44,13 @@ def self.openid_configuration_url client_config['openid_connect_server_url'] + '/.well-known/openid-configuration' end + def self.also_logout_from_oic_server? + client_config['also_logout_from_oic_server'] + end + def self.get_dynamic_config hash = Digest::SHA1.hexdigest client_config.to_json - expiry = client_config['dynamic_config_expiry'] || 86400 + expiry = (client_config['dynamic_config_expiry'] || 86400).to_i Rails.cache.fetch("oic_session_dynamic_#{hash}", expires_in: expiry) do HTTParty::Basement.default_options.update(verify: false) if client_config['disable_ssl_validation'] ActiveSupport::HashWithIndifferentAccess.new HTTParty.get(openid_configuration_url) @@ -134,6 +140,9 @@ def check_keycloak_role(role) if user["resource_access"].present? && user["resource_access"][client_config['client_id']].present? kc_is_in_role = user["resource_access"][client_config['client_id']]["roles"].include?(role) end + if user["groups"].present? + kc_is_in_role = user["groups"].include?(role) + end return true if kc_is_in_role end diff --git a/app/views/settings/_redmine_openid_connect_settings.html.erb b/app/views/settings/_redmine_openid_connect_settings.html.erb index de205ea..af83888 100644 --- a/app/views/settings/_redmine_openid_connect_settings.html.erb +++ b/app/views/settings/_redmine_openid_connect_settings.html.erb @@ -59,3 +59,13 @@ <%= select_tag 'settings[disallowed_auth_sources_login]', options_for_select(AuthSource.all.map { |a| [a.name, a.id] }, OicSession.disallowed_auth_sources_login), :multiple => true, :include_blank => true, :size => 5 %>
+ ++ + <%= check_box_tag 'settings[override_user_attributes]', true, @settings['override_user_attributes'] %> +
+ ++ + <%= check_box_tag 'settings[also_logout_from_oic_server]', false, @settings['also_logout_from_oic_server'] %> +
diff --git a/config/locales/de.yml b/config/locales/de.yml index e9cbcd3..98ec91d 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -12,7 +12,9 @@ de: dynamic_config_expiry: "Intervall für Aktualisierung der OpenID-Einstellungen (Default: 1 day)" create_user_if_not_exists: "Benutzer erstellen, falls nicht vorhanden" disallowed_auth_sources_login: "Benutzer aus den folgenden Authentifizierungsquellen müssen sich mit SSO anmelden" - oic_logout_success: 'Sie wurden ausgeloggt. Klicken Sie hier, um sich erneut einzuloggen.' + override_user_attributes: "Benutzerattribute bei jedem Login überschreiben" + also_logout_from_oic_server: "Auch vom OpenID Connect Server abmelden" + oic_logout_success: 'Sie wurden ausgeloggt. Klicken Sie hier, um sich erneut einzuloggen.' oic_cannot_create_user: "Der Benutzer %{value} konnte nicht angelegt werden: " oic_try_another_account: "Mit einem anderen Account einloggen." oic_cannot_login_user: "Benutzer %{value} konnte sich nicht anmelden: Bitte melden Sie sich mit der SSO-Option an" diff --git a/config/locales/en.yml b/config/locales/en.yml index 0aa1a7a..f8cc32a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -13,7 +13,9 @@ en: dynamic_config_expiry: How often to retrieve openid configuration (default 1 day) create_user_if_not_exists: Create user if not exists disallowed_auth_sources_login: Users from the following auth sources will be required to login with SSO - oic_logout_success: 'You have been logged out. Click here to log in again.' + override_user_attributes: Override user attributes at each login + also_logout_from_oic_server: Also logout from OpenID Connect server + oic_logout_success: 'You have been logged out. Click here to log in again.' oic_cannot_create_user: "Could not create the user %{value}: " oic_try_another_account: "Try logging in with another account" oic_cannot_login_user: "User %{value} could not login: Please login using the SSO option" diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 05a3c8c..cd9c433 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -13,7 +13,9 @@ pt: dynamic_config_expiry: "Com que frequência obter configuração do openid (padrão 1 dia)" create_user_if_not_exists: "Criar utilizador caso não exista" disallowed_auth_sources_login: "Utilizadores das fontes selecionadas deverão fazer login SSO" - oic_logout_success: 'Saiu com sucesso. Clique aqui para voltar a entrar.' + override_user_attributes: "Sobrescrever atributos do utilizador em cada login" + also_logout_from_oic_server: "Também sair do servidor OpenID Connect" + oic_logout_success: 'Saiu com sucesso. Clique aqui para voltar a entrar.' oic_cannot_create_user: "Não foi possível criar o utilizador %{value}: " oic_try_another_account: "Tente entrar com uma conta diferente" oic_cannot_login_user: "Não foi possível autenticar o utilizador %{value}: Por favor use o login SSO" diff --git a/init.rb b/init.rb index 9f9620f..141c516 100644 --- a/init.rb +++ b/init.rb @@ -1,7 +1,7 @@ -require 'redmine' -require_relative 'lib/redmine_openid_connect/application_controller_patch' -require_relative 'lib/redmine_openid_connect/account_controller_patch' -require_relative 'lib/redmine_openid_connect/hooks' +require "redmine" +require "#{File.dirname(__FILE__)}/lib/redmine_openid_connect/application_controller_patch" +require "#{File.dirname(__FILE__)}/lib/redmine_openid_connect/account_controller_patch" +require "#{File.dirname(__FILE__)}/lib/redmine_openid_connect/hooks" Redmine::Plugin.register :redmine_openid_connect do name 'Redmine Openid Connect plugin' @@ -13,8 +13,3 @@ settings :default => { 'empty' => true }, partial: 'settings/redmine_openid_connect_settings' end - - -ApplicationController.prepend(RedmineOpenidConnect::ApplicationControllerPatch) -AccountController.prepend(RedmineOpenidConnect::AccountControllerPatch) - diff --git a/lib/redmine_openid_connect/account_controller_patch.rb b/lib/redmine_openid_connect/account_controller_patch.rb index 94ec852..be2beb0 100644 --- a/lib/redmine_openid_connect/account_controller_patch.rb +++ b/lib/redmine_openid_connect/account_controller_patch.rb @@ -18,11 +18,16 @@ def logout oic_session.destroy logout_user reset_session - redirect_to oic_session.end_session_url if oic_session.end_session_url + + if OicSession.also_logout_from_oic_server? + redirect_to oic_session.end_session_url if oic_session.end_session_url + else + redirect_to oic_local_logout_url + end rescue ActiveRecord::RecordNotFound => e redirect_to oic_local_logout_url end - + # performs redirect to SSO server def oic_login if session[:oic_session_id].blank? @@ -64,7 +69,7 @@ def oic_local_login # verify request state or reauthorize unless oic_session.state == params[:state] - flash[:error] = "Requête OpenID Connect invalide." + flash[:error] = "Invalid OpenID Connect request." return redirect_to oic_local_logout end @@ -73,7 +78,7 @@ def oic_local_login # verify id token nonce or reauthorize if oic_session.id_token.present? unless oic_session.claims['nonce'] == oic_session.nonce - flash[:error] = "ID Token invalide." + flash[:error] = "ID Token invalid." return redirect_to oic_local_logout end end @@ -87,8 +92,32 @@ def oic_local_login return invalid_credentials end + username = user_info["user_name"] || user_info["nickname"] || user_info["preferred_username"] || user_info["username"] + # Check if there's already an existing user - user = User.find_by_mail(user_info["email"]) + user = User.find_by_login(username) + + if user.nil? + if !OicSession.create_user_if_not_exists? + flash.now[:warning] ||= l(:oic_cannot_create_user, user_info["email"]) + + logger.warn "Could not create user #{user_info["email"]}, the system is not allowed to create new users through openid" + flash.now[:warning] += "The system is not allowed to create new users through openid" + + return invalid_credentials + end + end + + firstname = user_info["given_name"] + lastname = user_info["family_name"] + + if (firstname.nil? || lastname.nil?) && user_info["name"] + parts = user_info["name"].split + if parts.length >= 2 + firstname = parts[0] + lastname = parts[-1] + end + end if user.nil? if !OicSession.create_user_if_not_exists? @@ -127,6 +156,9 @@ def oic_local_login if user.save user.update_attribute(:admin, oic_session.admin?) + + update_groups(user,user_info['groups']) + oic_session.user_id = user.id oic_session.save! # after user creation just show "My Page" don't redirect to remember @@ -141,6 +173,15 @@ def oic_local_login end else user.update_attribute(:admin, oic_session.admin?) + + if Setting.plugin_redmine_openid_connect['override_user_attributes'] + user.update_attribute(:firstname, firstname) + user.update_attribute(:lastname, lastname) + user.update_attribute(:mail, user_info["email"]) + end + + update_groups(user,user_info['groups']) + oic_session.user_id = user.id oic_session.save! # redirect back to initial URL @@ -193,5 +234,42 @@ def authorize_params end end end + + def update_groups(user, groups) + # Normalize group names + group_names = groups.map(&:to_s).map(&:downcase) + + # Load existing groups in one query + existing_groups = Group.where('LOWER(lastname) IN (?)', group_names).index_by { |g| g.lastname.downcase } + + group_names.each do |gname| + rm_g = existing_groups[gname] + + if rm_g + # Only add the user if not already in the group + unless rm_g.users.exists?(user.id) + rm_g.users << user + logger.info("[OIDC] Added user #{user.login} to group #{rm_g.name}") + else + logger.info("[OIDC] User #{user.login} already in group #{rm_g.name}") + end + else + # Group doesn’t exist → create and add user + g = Group.new(lastname: gname, name: gname) + g.users << user + if g.save + logger.info("[OIDC] Created group #{gname} and added user #{user.login}") + existing_groups[gname] = g + else + logger.error("[OIDC] Failed to create group #{gname}: #{g.errors.full_messages.join(', ')}") + end + end + rescue => e + logger.error("[OIDC] Error updating group #{gname} for user #{user.login}: #{e.message}") + end + end + end # AccountControllerPatch end + +AccountController.prepend RedmineOpenidConnect::AccountControllerPatch diff --git a/lib/redmine_openid_connect/application_controller_patch.rb b/lib/redmine_openid_connect/application_controller_patch.rb index db2b576..c60c781 100644 --- a/lib/redmine_openid_connect/application_controller_patch.rb +++ b/lib/redmine_openid_connect/application_controller_patch.rb @@ -29,3 +29,5 @@ def logged_user=(user) end end # ApplicationControllerPatch end + +ApplicationController.prepend RedmineOpenidConnect::ApplicationControllerPatch