commit
94e68bc951
@ -0,0 +1,42 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 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 API |
||||
class ParserStruct < ::Hashie::Mash |
||||
## |
||||
# TODO: Hashie::Mash extends from Hash and |
||||
# does not allow overriding any enumerable methods. |
||||
# |
||||
# This clashed with moving the queries services to BaseContracted, |
||||
# as we now use a +group_by+ attribute clashing with +Enumerable#group_by#. |
||||
# This redefines the method to ensure it works with queries, but does not solve the underlying issue. |
||||
def group_by |
||||
self[:group_by] |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,103 @@ |
||||
module Ldap |
||||
class BaseService |
||||
attr_reader :ldap |
||||
|
||||
def initialize(ldap) |
||||
@ldap = ldap |
||||
end |
||||
|
||||
def call |
||||
User.system.run_given do |
||||
OpenProject::Mutex.with_advisory_lock_transaction(ldap, 'import_users') do |
||||
perform |
||||
end |
||||
end |
||||
end |
||||
|
||||
def perform |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
# rubocop:disable Metrics/AbcSize |
||||
def synchronize_user(user, ldap_con) |
||||
Rails.logger.debug { "[LDAP user sync] Synchronizing user #{user.login}." } |
||||
|
||||
update_attributes = user_attributes(user.login, ldap_con) |
||||
if update_attributes.nil? && user.persisted? |
||||
Rails.logger.info { "Could not find user #{user.login} in #{ldap.name}. Locking the user." } |
||||
user.update_column(:status, Principal.statuses[:locked]) |
||||
end |
||||
return unless update_attributes |
||||
|
||||
if user.new_record? |
||||
try_to_create(update_attributes) |
||||
else |
||||
try_to_update(user, update_attributes) |
||||
end |
||||
end |
||||
# rubocop:enable Metrics/AbcSize |
||||
|
||||
# Try to create the user from attributes |
||||
def try_to_update(user, attrs) |
||||
call = Users::UpdateService |
||||
.new(model: user, user: User.system) |
||||
.call(attrs) |
||||
|
||||
if call.success? |
||||
# Ensure the user is activated |
||||
call.result.update_column(:status, Principal.statuses[:active]) |
||||
Rails.logger.info { "[LDAP user sync] User '#{call.result.login}' updated." } |
||||
else |
||||
Rails.logger.error { "[LDAP user sync] User '#{user.login}' could not be updated: #{call.message}" } |
||||
end |
||||
end |
||||
|
||||
def try_to_create(attrs) |
||||
call = Users::CreateService |
||||
.new(user: User.system) |
||||
.call(attrs) |
||||
|
||||
if call.success? |
||||
Rails.logger.info { "[LDAP user sync] User '#{call.result.login}' created." } |
||||
else |
||||
Rails.logger.error { "[LDAP user sync] User '#{attrs[:login]}' could not be created: #{call.message}" } |
||||
end |
||||
end |
||||
|
||||
## |
||||
# Get the user attributes of a single matching LDAP entry. |
||||
# |
||||
# If the login matches multiple entries, return nil and issue a warning. |
||||
# If the login does not match, returns nil |
||||
def user_attributes(login, ldap_con) |
||||
# Return the first matching user |
||||
entries = find_entries_by(login: login, ldap_con: ldap_con) |
||||
|
||||
if entries.count == 0 |
||||
Rails.logger.info { "[LDAP user sync] Did not find LDAP entry for #{login}" } |
||||
return |
||||
end |
||||
|
||||
if entries.count > 1 |
||||
Rails.logger.warn { "[LDAP user sync] Found multiple entries for #{login}: #{entries.map(&:dn)}. Skipping" } |
||||
return |
||||
end |
||||
|
||||
entries.first |
||||
end |
||||
|
||||
def find_entries_by(login:, ldap_con: new_ldap_connection) |
||||
ldap_con |
||||
.search( |
||||
base: ldap.base_dn, |
||||
filter: ldap.login_filter(login), |
||||
attributes: ldap.search_attributes(true) |
||||
) |
||||
.map { |entry| ldap.get_user_attributes_from_ldap_entry(entry).except(:dn) } |
||||
end |
||||
|
||||
def new_ldap_connection |
||||
ldap.instance_eval { initialize_ldap_con(account, account_password) } |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,30 @@ |
||||
module Ldap |
||||
class ImportUsersFromFilterService < BaseService |
||||
attr_reader :filter |
||||
|
||||
def initialize(ldap, filter) |
||||
super(ldap) |
||||
@filter = filter |
||||
end |
||||
|
||||
def perform |
||||
get_entries_from_filter do |entry| |
||||
attributes = ldap.get_user_attributes_from_ldap_entry(entry) |
||||
next if User.by_login(attributes[:login]).exists? |
||||
|
||||
try_to_create attributes.except(:dn) |
||||
end |
||||
end |
||||
|
||||
def get_entries_from_filter(&block) |
||||
ldap_con = new_ldap_connection |
||||
|
||||
ldap_con.search( |
||||
base: ldap.base_dn, |
||||
filter: filter & ldap.default_filter, |
||||
attributes: ldap.search_attributes(true), |
||||
&block |
||||
) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,35 @@ |
||||
module Ldap |
||||
class ImportUsersFromListService < BaseService |
||||
attr_reader :logins |
||||
|
||||
def initialize(ldap, logins) |
||||
super(ldap) |
||||
@logins = logins |
||||
end |
||||
|
||||
def perform |
||||
new_users = logins - existing_users |
||||
|
||||
Rails.logger.debug { "Importing LDAP user import for #{ldap.name} for #{new_users.count} new users." } |
||||
import! new_users |
||||
end |
||||
|
||||
def import!(new_users) |
||||
ldap_con = new_ldap_connection |
||||
|
||||
new_users.each do |login| |
||||
synchronize_user(User.new(login: login), ldap_con) |
||||
rescue ::AuthSource::Error => e |
||||
Rails.logger.error { "Failed to synchronize user #{ldap.name} due to LDAP error: #{e.message}" } |
||||
# Reset the LDAP connection |
||||
ldap_con = new_ldap_connection |
||||
rescue StandardError => e |
||||
Rails.logger.error { "Failed to synchronize user #{ldap.name}: #{e.message}" } |
||||
end |
||||
end |
||||
|
||||
def existing_users |
||||
User.where("LOWER(login) in (?)", logins.map(&:downcase)).pluck(:login) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,46 @@ |
||||
module Ldap |
||||
class SynchronizeUsersService < BaseService |
||||
attr_reader :logins |
||||
|
||||
def initialize(ldap, logins = nil) |
||||
super(ldap) |
||||
@logins = logins |
||||
end |
||||
|
||||
def call |
||||
Rails.logger.debug { "Start LDAP user synchronization for #{ldap.name}." } |
||||
User.system.run_given do |
||||
OpenProject::Mutex.with_advisory_lock_transaction(ldap, 'synchronize_users') do |
||||
synchronize! |
||||
end |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def synchronize! |
||||
ldap_con = new_ldap_connection |
||||
|
||||
applicable_users.find_each do |user| |
||||
synchronize_user(user, ldap_con) |
||||
rescue ::AuthSource::Error => e |
||||
Rails.logger.error { "Failed to synchronize user #{ldap.name} due to LDAP error: #{e.message}" } |
||||
# Reset the LDAP connection |
||||
ldap_con = new_ldap_connection |
||||
rescue StandardError => e |
||||
Rails.logger.error { "Failed to synchronize user #{ldap.name}: #{e.message}" } |
||||
end |
||||
end |
||||
|
||||
# Get the applicable users |
||||
# as the service can be called with just a subset of users |
||||
# from rake/external services. |
||||
def applicable_users |
||||
if logins.present? |
||||
ldap.users.where("LOWER(login) in (?)", logins.map(&:downcase)) |
||||
else |
||||
ldap.users |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,49 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2022 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 Ldap |
||||
class SynchronizationJob < ::Cron::CronJob |
||||
# Run once per night at 11:30pm |
||||
self.cron_expression = '30 23 * * *' |
||||
|
||||
def perform |
||||
run_user_sync |
||||
end |
||||
|
||||
private |
||||
|
||||
def run_user_sync |
||||
return if OpenProject::Configuration.ldap_users_disable_sync_job? |
||||
|
||||
::LdapAuthSource.find_each do |ldap| |
||||
Rails.logger.info { "[LDAP groups] Synchronizing users for LDAP connection #{ldap.name}" } |
||||
::Ldap::SynchronizeUsersService.new(ldap).perform |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
class AddIncludeSubprojectsToQuery < ActiveRecord::Migration[6.1] |
||||
def change |
||||
add_column :queries, |
||||
:include_subprojects, |
||||
:boolean, |
||||
null: false, |
||||
default: Setting.display_subprojects_work_packages? |
||||
|
||||
# Remove the default now |
||||
reversible do |dir| |
||||
dir.up { change_column_default :queries, :include_subprojects, nil } |
||||
end |
||||
end |
||||
end |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue