1# BEGIN BPS TAGGED BLOCK {{{ 2# 3# COPYRIGHT: 4# 5# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC 6# <sales@bestpractical.com> 7# 8# (Except where explicitly superseded by other copyright notices) 9# 10# 11# LICENSE: 12# 13# This work is made available to you under the terms of Version 2 of 14# the GNU General Public License. A copy of that license should have 15# been provided with this software, but in any event can be snarfed 16# from www.gnu.org. 17# 18# This work is distributed in the hope that it will be useful, but 19# WITHOUT ANY WARRANTY; without even the implied warranty of 20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 21# General Public License for more details. 22# 23# You should have received a copy of the GNU General Public License 24# along with this program; if not, write to the Free Software 25# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 26# 02110-1301 or visit their web page on the internet at 27# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. 28# 29# 30# CONTRIBUTION SUBMISSION POLICY: 31# 32# (The following paragraph is not intended to limit the rights granted 33# to you to modify and distribute this software under the terms of 34# the GNU General Public License and is only of importance to you if 35# you choose to contribute your changes and enhancements to the 36# community by submitting them to Best Practical Solutions, LLC.) 37# 38# By intentionally submitting any modifications, corrections or 39# derivatives to this work, or any other work intended for use with 40# Request Tracker, to Best Practical Solutions, LLC, you confirm that 41# you are the copyright holder for those contributions and you grant 42# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, 43# royalty-free, perpetual, license to use, copy, create derivative 44# works based on those contributions, and sublicense and distribute 45# those contributions and any derivatives thereof. 46# 47# END BPS TAGGED BLOCK }}} 48 49package RT::LDAPImport; 50 51use warnings; 52use strict; 53use base qw(Class::Accessor); 54__PACKAGE__->mk_accessors(qw(_ldap _group _users)); 55use Carp; 56use Net::LDAP; 57use Net::LDAP::Util qw(escape_filter_value); 58use Net::LDAP::Control::Paged; 59use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED); 60use Data::Dumper; 61 62=head1 NAME 63 64RT::LDAPImport - Import Users from an LDAP store 65 66=head1 SYNOPSIS 67 68In C<RT_SiteConfig.pm>: 69 70 Set($LDAPHost,'my.ldap.host'); 71 Set($LDAPOptions, [ port => 636, 72 scheme => 'ldaps', 73 raw => qr/(\;binary)/, 74 version => 3, 75 verify => 'required', 76 cafile => '/certificate-file/path' ]); 77 Set($LDAPUser,'me'); 78 Set($LDAPPassword,'mypass'); 79 Set($LDAPBase, 'ou=People,o=Our Place'); 80 Set($LDAPFilter, '(&(cn = users))'); 81 Set($LDAPMapping, {Name => 'uid', # required 82 EmailAddress => 'mail', 83 RealName => 'cn', 84 WorkPhone => 'telephoneNumber', 85 Organization => 'departmentName'}); 86 87 # If you want to sync Groups from LDAP into RT 88 89 Set($LDAPGroupBase, 'ou=Groups,o=Our Place'); 90 Set($LDAPGroupFilter, '(&(cn = Groups))'); 91 Set($LDAPGroupMapping, {Name => 'cn', 92 Member_Attr => 'member', 93 Member_Attr_Value => 'dn' }); 94 95Running the import: 96 97 # Run a test import 98 /opt/rt4/sbin/rt-ldapimport --verbose > ldapimport.debug 2>&1 99 100 # Run for real, possibly put in cron 101 /opt/rt4/sbin/rt-ldapimport --import 102 103=head1 CONFIGURATION 104 105All of the configuration for the importer goes in 106your F<RT_SiteConfig.pm> file. Some of these values pass through 107to L<Net::LDAP> so you can check there for valid values and more 108advanced options. 109 110=over 111 112=item C<< Set($LDAPHost,'our.ldap.host'); >> 113 114Hostname or ldap(s):// uri: 115 116=item C<< Set($LDAPOptions, [ port => 636 ]); >> 117 118This allows you to pass any options supported by the L<Net::LDAP> 119new method. 120 121=item C<< Set($LDAPUser, 'uid=foo,ou=users,dc=example,dc=com'); >> 122 123Your LDAP username or DN. If unset, we'll attempt an anonymous bind. 124 125=item C<< Set($LDAPPassword, 'ldap pass'); >> 126 127Your LDAP password. 128 129=item C<< Set($LDAPBase, 'ou=People,o=Our Place'); >> 130 131Base object to search from. 132 133=item C<< Set($LDAPFilter, '(&(cn = users))'); >> 134 135The LDAP search filter to apply (in this case, find all the users). 136 137=item C<< Set($LDAPMapping... >> 138 139 Set($LDAPMapping, {Name => 'uid', 140 EmailAddress => 'mail', 141 RealName => 'cn', 142 WorkPhone => 'telephoneNumber', 143 Organization => 'departmentName'}); 144 145This provides the mapping of attributes in RT to attribute(s) in LDAP. 146Only Name is required for RT. 147 148The values in the mapping (i.e. the LDAP fields, the right hand side) 149can be one of the following: 150 151=over 4 152 153=item an attribute 154 155LDAP attribute to use. Only first value is used if attribute is 156multivalue. For example: 157 158 EmailAddress => 'mail', 159 160=item an array reference 161 162The LDAP attributes can also be an arrayref of LDAP fields, 163for example: 164 165 WorkPhone => [qw/CompanyPhone Extension/] 166 167which will be concatenated together with a space. First values 168of each attribute are used in case they have multiple values. 169 170=item a subroutine reference 171 172The LDAP attribute can also be a subroutine reference that does 173mapping, for example: 174 175 YYY => sub { 176 my %args = @_; 177 my @values = grep defined && length, $args{ldap_entry}->get_value('XXX'); 178 return @values; 179 }, 180 181The subroutine should return value or list of values. The following 182arguments are passed into the function in a hash: 183 184=over 4 185 186=item self 187 188Instance of this class. 189 190=item ldap_entry 191 192L<Net::LDAP::Entry> instance that is currently mapped. 193 194=item import 195 196Boolean value indicating whether it's import or a dry run. If it's 197dry run (import is false) then function shouldn't change anything. 198 199=item mapping 200 201Hash reference with the currently processed mapping, eg. C<$LDAPMapping>. 202 203=item rt_field and ldap_field 204 205The currently processed key and value from the mapping. 206 207=item result 208 209Hash reference with results of completed mappings for this ldap entry. 210This should be used to inject that are not in the mapping, not to inspect. 211Mapping is processed in literal order of the keys. 212 213=back 214 215=back 216 217The keys in the mapping (i.e. the RT fields, the left hand side) may be a user 218custom field name prefixed with C<UserCF.>, for example C<< 'UserCF.Employee 219Number' => 'employeeId' >>. Note that this only B<adds> values at the moment, 220which on single value CFs will remove any old value first. Multiple value CFs 221may behave not quite how you expect. If the attribute no longer exists on a 222user in LDAP, it will be cleared on the RT side as well. 223 224You may also prefix any RT custom field name with C<CF.> inside your mapping to 225add available values to a Select custom field. This effectively takes user 226attributes in LDAP and adds the values as selectable options in a CF. It does 227B<not> set a CF value on any RT object (User, Ticket, Queue, etc). You might 228use this to populate a ticket Location CF with all the locations of your users 229so that tickets can be associated with the locations in use. 230 231=item C<< Set($LDAPCreatePrivileged, 1); >> 232 233By default users are created as Unprivileged, but you can change this by 234setting C<$LDAPCreatePrivileged> to 1. 235 236=item C<< Set($LDAPGroupName,'My Imported Users'); >> 237 238The RT Group new and updated users belong to. By default, all users 239added or updated by the importer will belong to the 'Imported from LDAP' 240group. 241 242=item C<< Set($LDAPSkipAutogeneratedGroup, 1); >> 243 244Set this to true to prevent users from being automatically 245added to the group configured by C<$LDAPGroupName>. 246 247=item C<< Set($LDAPUpdateUsers, 1); >> 248 249By default, existing users are skipped. If you 250turn on LDAPUpdateUsers, we will clobber existing 251data with data from LDAP. 252 253=item C<< Set($LDAPUpdateOnly, 1); >> 254 255By default, we create users who don't exist in RT but do 256match your LDAP filter and obey C<$LDAPUpdateUsers> for existing 257users. This setting updates existing users, overriding 258C<$LDAPUpdateUsers>, but won't create new 259users who are found in LDAP but not in RT. 260 261=item C<< Set($LDAPGroupBase, 'ou=Groups,o=Our Place'); >> 262 263Where to search for groups to import. 264 265=item C<< Set($LDAPGroupFilter, '(&(cn = Groups))'); >> 266 267The search filter to apply. 268 269=item C<< Set($LDAPGroupMapping... >> 270 271 Set($LDAPGroupMapping, {Name => 'cn', 272 Member_Attr => 'member', 273 Member_Attr_Value => 'dn' }); 274 275A mapping of RT attributes to LDAP attributes to identify group members. 276Name will become the name of the group in RT, in this case pulling 277from the cn attribute on the LDAP group record returned. Everything 278besides C<Member_Attr_Value> is processed according to rules described 279in documentation for C<$LDAPMapping> option, so value can be array 280or code reference besides scalar. 281 282C<Member_Attr> is the field in the LDAP group record the importer should 283look at for group members. These values (there may be multiple members) 284will then be compared to the RT user name, which came from the LDAP 285user record. See F<t/ldapimport/group-callbacks.t> for a complex example of 286using a code reference as value of this option. 287 288C<Member_Attr_Value>, which defaults to 'dn', specifies where on the LDAP 289user record the importer should look to compare the member value. 290A match between the member field on the group record and this 291identifier (dn or other LDAP field) on a user record means the 292user will be added to that group in RT. 293 294C<id> is the field in LDAP group record that uniquely identifies 295the group. This is optional and shouldn't be equal to mapping for 296Name field. Group names in RT must be distinct and you don't need 297another unique identifier in common situation. However, when you 298rename a group in LDAP, without this option set properly you end 299up with two groups in RT. 300 301You can provide a C<Description> key which will be added as the group 302description in RT. The default description is 'Imported from LDAP'. 303 304=item C<< Set($LDAPImportGroupMembers, 1); >> 305 306When disabled, the default, LDAP group import expects that all LDAP members 307already exist as RT users. Often the user import stage, which happens before 308groups, is used to create and/or update group members by using an 309C<$LDAPFilter> which includes a C<memberOf> attribute. 310 311When enabled, by setting to C<1>, LDAP group members are explicitly imported 312before membership is synced with RT. This enables groups-only configurations 313to also import group members without specifying a potentially long and complex 314C<$LDAPFilter> using C<memberOf>. It's particularly handy when C<memberOf> 315isn't available on user entries. 316 317Note that C<$LDAPFilter> still applies when this option is enabled, so some 318group members may be filtered out from the import. 319 320=item C<< Set($LDAPSizeLimit, 1000); >> 321 322You can set this value if your LDAP server has result size limits. 323 324=back 325 326=head1 Mapping Groups Between RT and LDAP 327 328If you are using the importer, you likely want to manage access via 329LDAP by putting people in groups like 'DBAs' and 'IT Support', but 330also have groups for other non-RT related things. In this case, you 331won't want to create all of your LDAP groups in RT. To limit the groups 332that get mirrored, construct your C<$LDAPGroupFilter> as an OR (|) with 333all of the RT groups you want to mirror from LDAP. For example: 334 335 Set($LDAPGroupBase, 'OU=Groups,OU=Company,DC=COM'); 336 Set($LDAPGroupFilter, '(|(CN=DBAs)(CN=IT Support))'); 337 338The importer will then import only the groups that match. In this case, 339import means: 340 341=over 342 343=item * Verifying the group is in AD; 344 345=item * Creating the group in RT if it doesn't exist; 346 347=item * Populating the group with the members identified in AD; 348 349=back 350 351The import script will also issue a warning if a user isn't found in RT, 352but this should only happen when testing. When running with --import on, 353users are created before groups are processed, so all users (group 354members) should exist unless there are inconsistencies in your LDAP configuration. 355 356=head1 Running the Import 357 358Executing C<rt-ldapimport> will run a test that connects to your LDAP server 359and prints out a list of the users found. To see more about these users, 360and to see more general debug information, include the C<--verbose> flag. 361 362That debug information is also sent to the RT log with the debug level. 363Errors are logged to the screen and to the RT log. 364 365Executing C<rt-ldapimport> with the C<--import> flag will cause it to import 366users into your RT database. It is recommended that you make a database 367backup before doing this. If your filters aren't set properly this could 368create a lot of users or groups in your RT instance. 369 370=head1 LDAP Filters 371 372The L<ldapsearch|http://www.openldap.org/software/man.cgi?query=ldapsearch&manpath=OpenLDAP+2.0-Release> 373utility in openldap can be very helpful while refining your filters. 374 375=head1 METHODS 376 377=head2 connect_ldap 378 379Relies on the config variables C<$LDAPHost>, C<$LDAPOptions>, C<$LDAPUser>, 380and C<$LDAPPassword> being set in your RT Config files. 381 382 Set($LDAPHost,'my.ldap.host'); 383 Set($LDAPOptions, [ port => 636 ]); 384 Set($LDAPUSER,'me'); 385 Set($LDAPPassword,'mypass'); 386 387LDAPUser and LDAPPassword can be blank, 388which will cause an anonymous bind. 389 390LDAPHost can be a hostname or an ldap:// ldaps:// uri. 391 392=cut 393 394sub connect_ldap { 395 my $self = shift; 396 397 $RT::LDAPOptions = [] unless $RT::LDAPOptions; 398 my $ldap = Net::LDAP->new($RT::LDAPHost, @$RT::LDAPOptions); 399 400 $RT::Logger->debug("connecting to $RT::LDAPHost"); 401 unless ($ldap) { 402 $RT::Logger->error("Can't connect to $RT::LDAPHost $@"); 403 return; 404 } 405 406 my $msg; 407 if ($RT::LDAPUser) { 408 $RT::Logger->debug("binding as $RT::LDAPUser"); 409 $msg = $ldap->bind($RT::LDAPUser, password => $RT::LDAPPassword); 410 } else { 411 $RT::Logger->debug("binding anonymously"); 412 $msg = $ldap->bind; 413 } 414 415 if ($msg->code) { 416 $RT::Logger->error("LDAP bind failed " . $msg->error); 417 return; 418 } 419 420 $self->_ldap($ldap); 421 return $ldap; 422 423} 424 425=head2 run_user_search 426 427Set up the appropriate arguments for a listing of users. 428 429=cut 430 431sub run_user_search { 432 my $self = shift; 433 $self->_run_search( 434 base => $RT::LDAPBase, 435 filter => $RT::LDAPFilter 436 ); 437 438} 439 440=head2 _run_search 441 442Executes a search using the provided base and filter. 443 444Will connect to LDAP server using C<connect_ldap>. 445 446Returns an array of L<Net::LDAP::Entry> objects, possibly consolidated from 447multiple LDAP pages. 448 449=cut 450 451sub _run_search { 452 my $self = shift; 453 my $ldap = $self->_ldap||$self->connect_ldap; 454 my %args = @_; 455 456 unless ($ldap) { 457 $RT::Logger->error("fetching an LDAP connection failed"); 458 return; 459 } 460 461 my %search = ( 462 base => $args{base}, 463 filter => $args{filter}, 464 scope => ($args{scope} || 'sub'), 465 ); 466 my (@results, $page, $cookie); 467 468 if ($RT::LDAPSizeLimit) { 469 $page = Net::LDAP::Control::Paged->new( size => $RT::LDAPSizeLimit, critical => 1 ); 470 $search{control} = $page; 471 } 472 473 LOOP: { 474 # Start where we left off 475 $page->cookie($cookie) if $page and $cookie; 476 477 $RT::Logger->debug("searching with: " . join(' ', map { "$_ => '$search{$_}'" } sort keys %search)); 478 479 my $result = $ldap->search( %search ); 480 481 if ($result->code) { 482 $RT::Logger->error("LDAP search failed " . $result->error); 483 last; 484 } 485 486 push @results, $result->entries; 487 488 # Short circuit early if we're done 489 last if not $result->count 490 or $result->count < ($RT::LDAPSizeLimit || 0); 491 492 if ($page) { 493 if (my $control = $result->control( LDAP_CONTROL_PAGED )) { 494 $cookie = $control->cookie; 495 } else { 496 $RT::Logger->error("LDAP search didn't return a paging control"); 497 last; 498 } 499 } 500 redo if $cookie; 501 } 502 503 # Let the server know we're abandoning the search if we errored out 504 if ($cookie) { 505 $RT::Logger->debug("Informing the LDAP server we're done with the result set"); 506 $page->cookie($cookie); 507 $page->size(0); 508 $ldap->search( %search ); 509 } 510 511 $RT::Logger->debug("search found ".scalar @results." objects"); 512 return @results; 513} 514 515=head2 import_users import => 1|0 516 517Takes the results of the search from run_search 518and maps attributes from LDAP into C<RT::User> attributes 519using C<$LDAPMapping>. 520Creates RT users if they don't already exist. 521 522With no arguments, only prints debugging information. 523Pass C<--import> to actually change data. 524 525C<$LDAPMapping>> should be set in your C<RT_SiteConfig.pm> 526file and look like this. 527 528 Set($LDAPMapping, { RTUserField => LDAPField, RTUserField => LDAPField }); 529 530RTUserField is the name of a field on an C<RT::User> object 531LDAPField can be a simple scalar and that attribute 532will be looked up in LDAP. 533 534It can also be an arrayref, in which case each of the 535elements will be evaluated in turn. Scalars will be 536looked up in LDAP and concatenated together with a single 537space. 538 539If the value is a sub reference, it will be executed. 540The sub should return a scalar, which will be examined. 541If it is a scalar, the value will be looked up in LDAP. 542If it is an arrayref, the values will be concatenated 543together with a single space. 544 545By default users are created as Unprivileged, but you can change this by 546setting C<$LDAPCreatePrivileged> to 1. 547 548=cut 549 550sub import_users { 551 my $self = shift; 552 my %args = @_; 553 554 $self->_users({}); 555 556 my @results = $self->run_user_search; 557 return $self->_import_users( %args, users => \@results ); 558} 559 560sub _import_users { 561 my $self = shift; 562 my %args = @_; 563 my $users = $args{users}; 564 565 unless ( @$users ) { 566 $RT::Logger->debug("No users found, no import"); 567 $self->disconnect_ldap; 568 return; 569 } 570 571 my $mapping = $RT::LDAPMapping; 572 return unless $self->_check_ldap_mapping( mapping => $mapping ); 573 574 my $done = 0; my $count = scalar @$users; 575 while (my $entry = shift @$users) { 576 my $user = $self->_build_user_object( ldap_entry => $entry ); 577 $self->_import_user( user => $user, ldap_entry => $entry, import => $args{import} ); 578 $done++; 579 $RT::Logger->debug("Imported $done/$count users"); 580 } 581 return 1; 582} 583 584=head2 _import_user 585 586We have found a user to attempt to import; returns the L<RT::User> 587object if it was found (or created), C<undef> if not. 588 589=cut 590 591sub _import_user { 592 my $self = shift; 593 my %args = @_; 594 595 unless ( $args{user}{Name} ) { 596 $RT::Logger->warn("No Name or Emailaddress for user, skipping ".Dumper($args{user})); 597 return; 598 } 599 if ( $args{user}{Name} =~ /^[0-9]+$/) { 600 $RT::Logger->debug("Skipping user '$args{user}{Name}', as it is numeric"); 601 return; 602 } 603 604 $RT::Logger->debug("Processing user $args{user}{Name}"); 605 $self->_cache_user( %args ); 606 607 $args{user} = $self->create_rt_user( %args ); 608 return unless $args{user}; 609 610 $self->add_user_to_group( %args ); 611 $self->add_custom_field_value( %args ); 612 $self->update_object_custom_field_values( %args, object => $args{user} ); 613 614 return $args{user}; 615} 616 617=head2 _cache_user ldap_entry => Net::LDAP::Entry, [user => { ... }] 618 619Adds the user to a global cache which is used when importing groups later. 620 621Optionally takes a second argument which is a user data object returned by 622_build_user_object. If not given, _cache_user will call _build_user_object 623itself. 624 625Returns the user Name. 626 627=cut 628 629sub _cache_user { 630 my $self = shift; 631 my %args = (@_); 632 my $user = $args{user} || $self->_build_user_object( ldap_entry => $args{ldap_entry} ); 633 634 $self->_users({}) if not defined $self->_users; 635 636 my $group_map = $RT::LDAPGroupMapping || {}; 637 my $member_attr_val = $group_map->{Member_Attr_Value} || 'dn'; 638 my $membership_key = lc $member_attr_val eq 'dn' 639 ? $args{ldap_entry}->dn 640 : $args{ldap_entry}->get_value($member_attr_val); 641 642 # Fallback to the DN if the user record doesn't have a value 643 unless (defined $membership_key) { 644 $membership_key = $args{ldap_entry}->dn; 645 $RT::Logger->warn("User attribute '$member_attr_val' has no value for '$membership_key'; falling back to DN"); 646 } 647 648 return $self->_users->{lc $membership_key} = $user->{Name}; 649} 650 651sub _show_user_info { 652 my $self = shift; 653 my %args = @_; 654 my $user = $args{user}; 655 my $rt_user = $args{rt_user}; 656 657 $RT::Logger->debug( "\tRT Field\tRT Value -> LDAP Value" ); 658 foreach my $key (sort keys %$user) { 659 my $old_value; 660 if ($rt_user) { 661 eval { $old_value = $rt_user->$key() }; 662 if ($user->{$key} && defined $old_value && $old_value eq $user->{$key}) { 663 $old_value = 'unchanged'; 664 } 665 } 666 $old_value ||= 'unset'; 667 $RT::Logger->debug( "\t$key\t$old_value => $user->{$key}" ); 668 } 669 #$RT::Logger->debug(Dumper($user)); 670} 671 672=head2 _check_ldap_mapping 673 674Returns true is there is an C<LDAPMapping> configured, 675returns false, logs an error and disconnects from 676ldap if there is no mapping. 677 678=cut 679 680sub _check_ldap_mapping { 681 my $self = shift; 682 my %args = @_; 683 my $mapping = $args{mapping}; 684 685 my @rtfields = keys %{$mapping}; 686 unless ( @rtfields ) { 687 $RT::Logger->error("No mapping found, can't import"); 688 $self->disconnect_ldap; 689 return; 690 } 691 692 return 1; 693} 694 695=head2 _build_user_object 696 697Utility method which wraps C<_build_object> to provide sane 698defaults for building users. It also tries to ensure a Name 699exists in the returned object. 700 701=cut 702 703sub _build_user_object { 704 my $self = shift; 705 my $user = $self->_build_object( 706 skip => qr/(?i)^(?:User)?CF\./, 707 mapping => $RT::LDAPMapping, 708 @_ 709 ); 710 $user->{Name} ||= $user->{EmailAddress}; 711 return $user; 712} 713 714=head2 _build_object 715 716Internal method - a wrapper around L</_parse_ldap_mapping> 717that flattens results turning every value into a scalar. 718 719The following: 720 721 [ 722 [$first_value1, ... ], 723 [$first_value2], 724 $scalar_value, 725 ] 726 727Turns into: 728 729 "$first_value1 $first_value2 $scalar_value" 730 731Arguments are just passed into L</_parse_ldap_mapping>. 732 733=cut 734 735sub _build_object { 736 my $self = shift; 737 my %args = @_; 738 739 my $res = $self->_parse_ldap_mapping( %args ); 740 foreach my $value ( values %$res ) { 741 @$value = map { ref $_ eq 'ARRAY'? $_->[0] : $_ } @$value; 742 $value = join ' ', grep defined && length, @$value; 743 } 744 return $res; 745} 746 747=head3 _parse_ldap_mapping 748 749Internal helper method that maps an LDAP entry to a hash 750according to passed arguments. Takes named arguments: 751 752=over 4 753 754=item ldap_entry 755 756L<Net::LDAP::Entry> instance that should be mapped. 757 758=item only 759 760Optional regular expression. If passed then only matching 761entries in the mapping will be processed. 762 763=item skip 764 765Optional regular expression. If passed then matching 766entries in the mapping will be skipped. 767 768=item mapping 769 770Hash that defines how to map. Key defines position 771in the result. Value can be one of the following: 772 773If we're passed a scalar or an array reference then 774value is: 775 776 [ 777 [value1_of_attr1, value2_of_attr1], 778 [value1_of_attr2, value2_of_attr2], 779 ] 780 781If we're passed a subroutine reference as value or 782as an element of array, it executes the code 783and returned list is pushed into results array: 784 785 [ 786 @result_of_function, 787 ] 788 789All arguments are passed into the subroutine as well 790as a few more. See more in description of C<$LDAPMapping> 791option. 792 793=back 794 795Returns hash reference with results, each value is 796an array with elements either scalars or arrays as 797described above. 798 799=cut 800 801sub _parse_ldap_mapping { 802 my $self = shift; 803 my %args = @_; 804 805 my $mapping = $args{mapping}; 806 807 my %res; 808 foreach my $rtfield ( sort keys %$mapping ) { 809 next if $args{'skip'} && $rtfield =~ $args{'skip'}; 810 next if $args{'only'} && $rtfield !~ $args{'only'}; 811 812 my $ldap_field = $mapping->{$rtfield}; 813 my @list = grep defined && length, ref $ldap_field eq 'ARRAY'? @$ldap_field : ($ldap_field); 814 unless (@list) { 815 $RT::Logger->error("Invalid LDAP mapping for $rtfield, no defined fields"); 816 next; 817 } 818 819 my @values; 820 foreach my $e (@list) { 821 if (ref $e eq 'CODE') { 822 push @values, $e->( 823 %args, 824 self => $self, 825 rt_field => $rtfield, 826 ldap_field => $ldap_field, 827 result => \%res, 828 ); 829 } elsif (ref $e) { 830 $RT::Logger->error("Invalid type of LDAP mapping for $rtfield, value is $e"); 831 next; 832 } else { 833 # XXX: get_value asref returns undef if there is no such field on 834 # the entry, should we warn? 835 push @values, grep defined, $args{'ldap_entry'}->get_value( $e, asref => 1 ); 836 } 837 } 838 $res{ $rtfield } = \@values; 839 } 840 841 return \%res; 842} 843 844=head2 create_rt_user 845 846Takes a hashref of args to pass to C<RT::User::Create> 847Will try loading the user and will only create a new 848user if it can't find an existing user with the C<Name> 849or C<EmailAddress> arg passed in. 850 851If the C<$LDAPUpdateUsers> variable is true, data in RT 852will be clobbered with data in LDAP. Otherwise we 853will skip to the next user. 854 855If C<$LDAPUpdateOnly> is true, we will not create new users 856but we will update existing ones. 857 858=cut 859 860sub create_rt_user { 861 my $self = shift; 862 my %args = @_; 863 my $user = $args{user}; 864 865 my $user_obj = $self->_load_rt_user(%args); 866 867 if ($user_obj->Id) { 868 my $message = "User $user->{Name} already exists as ".$user_obj->Id; 869 if ($RT::LDAPUpdateUsers || $RT::LDAPUpdateOnly) { 870 $RT::Logger->debug("$message, updating their data"); 871 if ($args{import}) { 872 my @results = $user_obj->Update( ARGSRef => $user, AttributesRef => [keys %$user] ); 873 $RT::Logger->debug(join("\n",@results)||'no change'); 874 } else { 875 $RT::Logger->debug("Found existing user $user->{Name} to update"); 876 $self->_show_user_info( %args, rt_user => $user_obj ); 877 } 878 } else { 879 $RT::Logger->debug("$message, skipping"); 880 } 881 } else { 882 if ( $RT::LDAPUpdateOnly ) { 883 $RT::Logger->debug("User $user->{Name} doesn't exist in RT, skipping"); 884 return; 885 } else { 886 if ($args{import}) { 887 my ($val, $msg) = $user_obj->Create( %$user, Privileged => $RT::LDAPCreatePrivileged ? 1 : 0 ); 888 889 unless ($val) { 890 $RT::Logger->error("couldn't create user_obj for $user->{Name}: $msg"); 891 return; 892 } 893 $RT::Logger->debug("Created user for $user->{Name} with id ".$user_obj->Id); 894 } else { 895 $RT::Logger->debug( "Found new user $user->{Name} to create in RT" ); 896 $self->_show_user_info( %args ); 897 return; 898 } 899 } 900 } 901 902 unless ($user_obj->Id) { 903 $RT::Logger->error("We couldn't find or create $user->{Name}. This should never happen"); 904 } 905 return $user_obj; 906 907} 908 909sub _load_rt_user { 910 my $self = shift; 911 my %args = @_; 912 my $user = $args{user}; 913 914 my $user_obj = RT::User->new($RT::SystemUser); 915 916 $user_obj->Load( $user->{Name} ); 917 unless ($user_obj->Id) { 918 $user_obj->LoadByEmail( $user->{EmailAddress} ); 919 } 920 921 return $user_obj; 922} 923 924=head2 add_user_to_group 925 926Adds new users to the group specified in the C<$LDAPGroupName> 927variable (defaults to 'Imported from LDAP'). 928You can avoid this if you set C<$LDAPSkipAutogeneratedGroup>. 929 930=cut 931 932sub add_user_to_group { 933 my $self = shift; 934 my %args = @_; 935 my $user = $args{user}; 936 937 return if $RT::LDAPSkipAutogeneratedGroup; 938 939 my $group = $self->_group||$self->setup_group; 940 941 my $principal = $user->PrincipalObj; 942 943 if ($group->HasMember($principal)) { 944 $RT::Logger->debug($user->Name . " already a member of " . $group->Name); 945 return; 946 } 947 948 if ($args{import}) { 949 my ($status, $msg) = $group->AddMember($principal->Id); 950 if ($status) { 951 $RT::Logger->debug("Added ".$user->Name." to ".$group->Name." [$msg]"); 952 } else { 953 $RT::Logger->error("Couldn't add ".$user->Name." to ".$group->Name." [$msg]"); 954 } 955 return $status; 956 } else { 957 $RT::Logger->debug("Would add to ".$group->Name); 958 return; 959 } 960} 961 962=head2 setup_group 963 964Pulls the C<$LDAPGroupName> object out of the DB or 965creates it if we need to do so. 966 967=cut 968 969sub setup_group { 970 my $self = shift; 971 my $group_name = $RT::LDAPGroupName||'Imported from LDAP'; 972 my $group = RT::Group->new($RT::SystemUser); 973 974 $group->LoadUserDefinedGroup( $group_name ); 975 unless ($group->Id) { 976 my ($id,$msg) = $group->CreateUserDefinedGroup( Name => $group_name ); 977 unless ($id) { 978 $RT::Logger->error("Can't create group $group_name [$msg]") 979 } 980 } 981 982 $self->_group($group); 983} 984 985=head3 add_custom_field_value 986 987Adds values to a Select (one|many) Custom Field. 988The Custom Field should already exist, otherwise 989this will throw an error and not import any data. 990 991This could probably use some caching. 992 993=cut 994 995sub add_custom_field_value { 996 my $self = shift; 997 my %args = @_; 998 my $user = $args{user}; 999 1000 my $data = $self->_build_object( 1001 %args, 1002 only => qr/^CF\.(.+)$/i, 1003 mapping => $RT::LDAPMapping, 1004 ); 1005 1006 foreach my $rtfield ( keys %$data ) { 1007 next unless $rtfield =~ /^CF\.(.+)$/i; 1008 my $cf_name = $1; 1009 1010 my $cfv_name = $data->{ $rtfield } 1011 or next; 1012 1013 my $cf = RT::CustomField->new($RT::SystemUser); 1014 my ($status, $msg) = $cf->Load($cf_name); 1015 unless ($status) { 1016 $RT::Logger->error("Couldn't load CF [$cf_name]: $msg"); 1017 next; 1018 } 1019 1020 my $cfv = RT::CustomFieldValue->new($RT::SystemUser); 1021 $cfv->LoadByCols( CustomField => $cf->id, 1022 Name => $cfv_name ); 1023 if ($cfv->id) { 1024 $RT::Logger->debug("Custom Field '$cf_name' already has '$cfv_name' for a value"); 1025 next; 1026 } 1027 1028 if ($args{import}) { 1029 ($status, $msg) = $cf->AddValue( Name => $cfv_name ); 1030 if ($status) { 1031 $RT::Logger->debug("Added '$cfv_name' to Custom Field '$cf_name' [$msg]"); 1032 } else { 1033 $RT::Logger->error("Couldn't add '$cfv_name' to '$cf_name' [$msg]"); 1034 } 1035 } else { 1036 $RT::Logger->debug("Would add '$cfv_name' to Custom Field '$cf_name'"); 1037 } 1038 } 1039 1040 return; 1041 1042} 1043 1044=head3 update_object_custom_field_values 1045 1046Adds CF values to an object (currently only users). The Custom Field should 1047already exist, otherwise this will throw an error and not import any data. 1048 1049Note that this code only B<adds> values at the moment, which on single value 1050CFs will remove any old value first. Multiple value CFs may behave not quite 1051how you expect. 1052 1053=cut 1054 1055sub update_object_custom_field_values { 1056 my $self = shift; 1057 my %args = @_; 1058 my $obj = $args{object}; 1059 1060 my $data = $self->_build_object( 1061 %args, 1062 only => qr/^UserCF\.(.+)$/i, 1063 mapping => $RT::LDAPMapping, 1064 ); 1065 1066 foreach my $rtfield ( sort keys %$data ) { 1067 # XXX TODO: accept GroupCF when we call this from group_import too 1068 next unless $rtfield =~ /^UserCF\.(.+)$/i; 1069 my $cf_name = $1; 1070 my $value = $data->{$rtfield}; 1071 $value = '' unless defined $value; 1072 1073 my $current = $obj->FirstCustomFieldValue($cf_name); 1074 $current = '' unless defined $current; 1075 1076 if (not length $current and not length $value) { 1077 $RT::Logger->debug("\tCF.$cf_name\tskipping, no value in RT and LDAP"); 1078 next; 1079 } 1080 elsif ($current eq $value) { 1081 $RT::Logger->debug("\tCF.$cf_name\tunchanged => $value"); 1082 next; 1083 } 1084 1085 $current = 'unset' unless length $current; 1086 $RT::Logger->debug("\tCF.$cf_name\t$current => $value"); 1087 next unless $args{import}; 1088 1089 my ($ok, $msg) = $obj->AddCustomFieldValue( Field => $cf_name, Value => $value ); 1090 $RT::Logger->error($obj->Name . ": Couldn't add value '$value' for '$cf_name': $msg") 1091 unless $ok; 1092 } 1093} 1094 1095=head2 import_groups import => 1|0 1096 1097Takes the results of the search from C<run_group_search> 1098and maps attributes from LDAP into C<RT::Group> attributes 1099using C<$LDAPGroupMapping>. 1100 1101Creates groups if they don't exist. 1102 1103Removes users from groups if they have been removed from the group on LDAP. 1104 1105With no arguments, only prints debugging information. 1106Pass C<--import> to actually change data. 1107 1108=cut 1109 1110sub import_groups { 1111 my $self = shift; 1112 my %args = @_; 1113 1114 my @results = $self->run_group_search; 1115 unless ( @results ) { 1116 $RT::Logger->debug("No results found, no group import"); 1117 $self->disconnect_ldap; 1118 return; 1119 } 1120 1121 my $mapping = $RT::LDAPGroupMapping; 1122 return unless $self->_check_ldap_mapping( mapping => $mapping ); 1123 1124 my $done = 0; my $count = scalar @results; 1125 while (my $entry = shift @results) { 1126 my $group = $self->_parse_ldap_mapping( 1127 %args, 1128 ldap_entry => $entry, 1129 skip => qr/^Member_Attr_Value$/i, 1130 mapping => $mapping, 1131 ); 1132 foreach my $key ( grep !/^Member_Attr/, keys %$group ) { 1133 @{ $group->{$key} } = map { ref $_ eq 'ARRAY'? $_->[0] : $_ } @{ $group->{$key} }; 1134 $group->{$key} = join ' ', grep defined && length, @{ $group->{$key} }; 1135 } 1136 @{ $group->{'Member_Attr'} } = map { ref $_ eq 'ARRAY'? @$_ : $_ } @{ $group->{'Member_Attr'} } 1137 if $group->{'Member_Attr'}; 1138 $group->{Description} ||= 'Imported from LDAP'; 1139 unless ( $group->{Name} ) { 1140 $RT::Logger->warn("No Name for group, skipping ".Dumper $group); 1141 next; 1142 } 1143 if ( $group->{Name} =~ /^[0-9]+$/) { 1144 $RT::Logger->debug("Skipping group '$group->{Name}', as it is numeric"); 1145 next; 1146 } 1147 $self->_import_group( %args, group => $group, ldap_entry => $entry ); 1148 $done++; 1149 $RT::Logger->debug("Imported $done/$count groups"); 1150 } 1151 return 1; 1152} 1153 1154=head3 run_group_search 1155 1156Set up the appropriate arguments for a listing of users. 1157 1158=cut 1159 1160sub run_group_search { 1161 my $self = shift; 1162 1163 unless ($RT::LDAPGroupBase && $RT::LDAPGroupFilter) { 1164 $RT::Logger->warn("Not running a group import, configuration not set"); 1165 return; 1166 } 1167 $self->_run_search( 1168 base => $RT::LDAPGroupBase, 1169 filter => $RT::LDAPGroupFilter 1170 ); 1171 1172} 1173 1174 1175=head2 _import_group 1176 1177The user has run us with C<--import>, so bring data in. 1178 1179=cut 1180 1181sub _import_group { 1182 my $self = shift; 1183 my %args = @_; 1184 my $group = $args{group}; 1185 my $ldap_entry = $args{ldap_entry}; 1186 1187 $RT::Logger->debug("Processing group $group->{Name}"); 1188 my ($group_obj, $created) = $self->create_rt_group( %args, group => $group ); 1189 return if $args{import} and not $group_obj; 1190 $self->add_group_members( 1191 %args, 1192 name => $group->{Name}, 1193 info => $group, 1194 group => $group_obj, 1195 ldap_entry => $ldap_entry, 1196 new => $created, 1197 ); 1198 # XXX TODO: support OCFVs for groups too 1199 return; 1200} 1201 1202=head2 create_rt_group 1203 1204Takes a hashref of args to pass to C<RT::Group::Create> 1205Will try loading the group and will only create a new 1206group if it can't find an existing group with the C<Name> 1207or C<EmailAddress> arg passed in. 1208 1209If C<$LDAPUpdateOnly> is true, we will not create new groups 1210but we will update existing ones. 1211 1212There is currently no way to prevent Group data from being 1213clobbered from LDAP. 1214 1215=cut 1216 1217sub create_rt_group { 1218 my $self = shift; 1219 my %args = @_; 1220 my $group = $args{group}; 1221 1222 my $group_obj = $self->find_rt_group(%args); 1223 return unless defined $group_obj; 1224 1225 $group = { map { $_ => $group->{$_} } qw(id Name Description) }; 1226 1227 my $id = delete $group->{'id'}; 1228 1229 my $created; 1230 if ($group_obj->Id) { 1231 if ($args{import}) { 1232 $RT::Logger->debug("Group $group->{Name} already exists as ".$group_obj->Id.", updating their data"); 1233 my @results = $group_obj->Update( ARGSRef => $group, AttributesRef => [keys %$group] ); 1234 $RT::Logger->debug(join("\n",@results)||'no change'); 1235 } else { 1236 $RT::Logger->debug( "Found existing group $group->{Name} to update" ); 1237 $self->_show_group_info( %args, rt_group => $group_obj ); 1238 } 1239 } else { 1240 if ( $RT::LDAPUpdateOnly ) { 1241 $RT::Logger->debug("Group $group->{Name} doesn't exist in RT, skipping"); 1242 return; 1243 } 1244 1245 if ($args{import}) { 1246 my ($val, $msg) = $group_obj->CreateUserDefinedGroup( %$group ); 1247 unless ($val) { 1248 $RT::Logger->error("couldn't create group_obj for $group->{Name}: $msg"); 1249 return; 1250 } 1251 $created = $val; 1252 $RT::Logger->debug("Created group for $group->{Name} with id ".$group_obj->Id); 1253 1254 if ( $id ) { 1255 my ($val, $msg) = $group_obj->SetAttribute( Name => 'LDAPImport-gid-'.$id, Content => 1 ); 1256 unless ($val) { 1257 $RT::Logger->error("couldn't set attribute: $msg"); 1258 return; 1259 } 1260 } 1261 1262 } else { 1263 $RT::Logger->debug( "Found new group $group->{Name} to create in RT" ); 1264 $self->_show_group_info( %args ); 1265 return; 1266 } 1267 } 1268 1269 unless ($group_obj->Id) { 1270 $RT::Logger->error("We couldn't find or create $group->{Name}. This should never happen"); 1271 } 1272 return ($group_obj, $created); 1273 1274} 1275 1276=head3 find_rt_group 1277 1278Loads groups by Name and by the specified LDAP id. Attempts to resolve 1279renames and other out-of-sync failures between RT and LDAP. 1280 1281=cut 1282 1283sub find_rt_group { 1284 my $self = shift; 1285 my %args = @_; 1286 my $group = $args{group}; 1287 1288 my $group_obj = RT::Group->new($RT::SystemUser); 1289 $group_obj->LoadUserDefinedGroup( $group->{Name} ); 1290 return $group_obj unless $group->{'id'}; 1291 1292 unless ( $group_obj->id ) { 1293 $RT::Logger->debug("No group in RT named $group->{Name}. Looking by $group->{id} LDAP id."); 1294 $group_obj = $self->find_rt_group_by_ldap_id( $group->{'id'} ); 1295 unless ( $group_obj ) { 1296 $RT::Logger->debug("No group in RT with LDAP id $group->{id}. Creating a new one."); 1297 return RT::Group->new($RT::SystemUser); 1298 } 1299 1300 $RT::Logger->debug("No group in RT named $group->{Name}, but found group by LDAP id $group->{id}. Renaming the group."); 1301 # $group->Update will take care of the name 1302 return $group_obj; 1303 } 1304 1305 my $attr_name = 'LDAPImport-gid-'. $group->{'id'}; 1306 my $rt_gid = $group_obj->FirstAttribute( $attr_name ); 1307 return $group_obj if $rt_gid; 1308 1309 my $other_group = $self->find_rt_group_by_ldap_id( $group->{'id'} ); 1310 if ( $other_group ) { 1311 $RT::Logger->debug("Group with LDAP id $group->{id} exists, as well as group named $group->{Name}. Renaming both."); 1312 } 1313 elsif ( grep $_->Name =~ /^LDAPImport-gid-/, @{ $group_obj->Attributes->ItemsArrayRef } ) { 1314 $RT::Logger->debug("No group in RT with LDAP id $group->{id}, but group $group->{Name} has id. Renaming the group and creating a new one."); 1315 } 1316 else { 1317 $RT::Logger->debug("No group in RT with LDAP id $group->{id}, but group $group->{Name} exists and has no LDAP id. Assigning the id to the group."); 1318 if ( $args{import} ) { 1319 my ($status, $msg) = $group_obj->SetAttribute( Name => $attr_name, Content => 1 ); 1320 unless ( $status ) { 1321 $RT::Logger->error("Couldn't set attribute: $msg"); 1322 return undef; 1323 } 1324 $RT::Logger->debug("Assigned $group->{id} LDAP group id to $group->{Name}"); 1325 } 1326 else { 1327 $RT::Logger->debug( "Group $group->{'Name'} gets LDAP id $group->{id}" ); 1328 } 1329 1330 return $group_obj; 1331 } 1332 1333 # rename existing group to move it out of our way 1334 { 1335 my ($old, $new) = ($group_obj->Name, $group_obj->Name .' (LDAPImport '. time . ')'); 1336 if ( $args{import} ) { 1337 my ($status, $msg) = $group_obj->SetName( $new ); 1338 unless ( $status ) { 1339 $RT::Logger->error("Couldn't rename group from $old to $new: $msg"); 1340 return undef; 1341 } 1342 $RT::Logger->debug("Renamed group $old to $new"); 1343 } 1344 else { 1345 $RT::Logger->debug( "Group $old to be renamed to $new" ); 1346 } 1347 } 1348 1349 return $other_group || RT::Group->new($RT::SystemUser); 1350} 1351 1352=head3 find_rt_group_by_ldap_id 1353 1354Loads an RT::Group by the ldap provided id (different from RT's internal group 1355id) 1356 1357=cut 1358 1359sub find_rt_group_by_ldap_id { 1360 my $self = shift; 1361 my $id = shift; 1362 1363 my $groups = RT::Groups->new( RT->SystemUser ); 1364 $groups->LimitToUserDefinedGroups; 1365 my $attr_alias = $groups->Join( FIELD1 => 'id', TABLE2 => 'Attributes', FIELD2 => 'ObjectId' ); 1366 $groups->Limit( ALIAS => $attr_alias, FIELD => 'ObjectType', VALUE => 'RT::Group' ); 1367 $groups->Limit( ALIAS => $attr_alias, FIELD => 'Name', VALUE => 'LDAPImport-gid-'. $id ); 1368 return $groups->First; 1369} 1370 1371 1372=head3 add_group_members 1373 1374Iterate over the list of values in the C<Member_Attr> LDAP entry. 1375Look up the appropriate username from LDAP. 1376Add those users to the group. 1377Remove members of the RT Group who are no longer members 1378of the LDAP group. 1379 1380=cut 1381 1382sub add_group_members { 1383 my $self = shift; 1384 my %args = @_; 1385 my $group = $args{group}; 1386 my $groupname = $args{name}; 1387 my $ldap_entry = $args{ldap_entry}; 1388 1389 $RT::Logger->debug("Processing group membership for $groupname"); 1390 1391 my $members = $args{'info'}{'Member_Attr'}; 1392 unless (defined $members) { 1393 $RT::Logger->warn("No members found for $groupname in Member_Attr"); 1394 return; 1395 } 1396 1397 if ($RT::LDAPImportGroupMembers) { 1398 $RT::Logger->debug("Importing members of group $groupname"); 1399 my @entries; 1400 my $attr = lc($RT::LDAPGroupMapping->{Member_Attr_Value} || 'dn'); 1401 1402 # Lookup each DN's full entry, or... 1403 if ($attr eq 'dn') { 1404 @entries = grep defined, map { 1405 my @results = $self->_run_search( 1406 scope => 'base', 1407 base => $_, 1408 filter => $RT::LDAPFilter, 1409 ); 1410 $results[0] 1411 } @$members; 1412 } 1413 # ...or find all the entries in a single search by attribute. 1414 else { 1415 # I wonder if this will run into filter length limits? -trs, 22 Jan 2014 1416 my $members = join "", map { "($attr=" . escape_filter_value($_) . ")" } @$members; 1417 @entries = $self->_run_search( 1418 base => $RT::LDAPBase, 1419 filter => "(&$RT::LDAPFilter(|$members))", 1420 ); 1421 } 1422 $self->_import_users( 1423 import => $args{import}, 1424 users => \@entries, 1425 ) or $RT::Logger->debug("Importing group members failed"); 1426 } 1427 1428 my %rt_group_members; 1429 if ($args{group} and not $args{new}) { 1430 my $user_members = $group->UserMembersObj( Recursively => 0); 1431 1432 # find members who are Disabled too so we don't try to add them below 1433 $user_members->FindAllRows; 1434 1435 while ( my $member = $user_members->Next ) { 1436 $rt_group_members{$member->Name} = $member; 1437 } 1438 } elsif (not $args{import}) { 1439 $RT::Logger->debug("No group in RT, would create with members:"); 1440 } 1441 1442 my $users = $self->_users; 1443 foreach my $member (@$members) { 1444 my $username; 1445 if (exists $users->{lc $member}) { 1446 next unless $username = $users->{lc $member}; 1447 } else { 1448 my $attr = lc($RT::LDAPGroupMapping->{Member_Attr_Value} || 'dn'); 1449 my $base = $attr eq 'dn' ? $member : $RT::LDAPBase; 1450 my $scope = $attr eq 'dn' ? 'base' : 'sub'; 1451 my $filter = $attr eq 'dn' 1452 ? $RT::LDAPFilter 1453 : "(&$RT::LDAPFilter($attr=" . escape_filter_value($member) . "))"; 1454 my @results = $self->_run_search( 1455 base => $base, 1456 scope => $scope, 1457 filter => $filter, 1458 ); 1459 unless ( @results ) { 1460 $users->{lc $member} = undef; 1461 $RT::Logger->error("No user found for $member who should be a member of $groupname"); 1462 next; 1463 } 1464 my $ldap_user = shift @results; 1465 $username = $self->_cache_user( ldap_entry => $ldap_user ); 1466 } 1467 if ( delete $rt_group_members{$username} ) { 1468 $RT::Logger->debug("\t$username\tin RT and LDAP"); 1469 next; 1470 } 1471 $RT::Logger->debug($group ? "\t$username\tin LDAP, adding to RT" : "\t$username"); 1472 next unless $args{import}; 1473 1474 my $rt_user = RT::User->new($RT::SystemUser); 1475 my ($res,$msg) = $rt_user->Load( $username ); 1476 unless ($res) { 1477 $RT::Logger->warn("Unable to load $username: $msg"); 1478 next; 1479 } 1480 ($res,$msg) = $group->AddMember($rt_user->PrincipalObj->Id); 1481 unless ($res) { 1482 $RT::Logger->warn("Failed to add $username to $groupname: $msg"); 1483 } 1484 } 1485 1486 for my $username (sort keys %rt_group_members) { 1487 $RT::Logger->debug("\t$username\tin RT, not in LDAP, removing"); 1488 next unless $args{import}; 1489 1490 my ($res,$msg) = $group->DeleteMember($rt_group_members{$username}->PrincipalObj->Id); 1491 unless ($res) { 1492 $RT::Logger->warn("Failed to remove $username to $groupname: $msg"); 1493 } 1494 } 1495} 1496 1497=head2 _show_group 1498 1499Show debugging information about the group record we're going to import 1500when the groups reruns us with C<--import>. 1501 1502=cut 1503 1504sub _show_group { 1505 my $self = shift; 1506 my %args = @_; 1507 my $group = $args{group}; 1508 1509 my $rt_group = RT::Group->new($RT::SystemUser); 1510 $rt_group->LoadUserDefinedGroup( $group->{Name} ); 1511 1512 if ( $rt_group->Id ) { 1513 $RT::Logger->debug( "Found existing group $group->{Name} to update" ); 1514 $self->_show_group_info( %args, rt_group => $rt_group ); 1515 } else { 1516 $RT::Logger->debug( "Found new group $group->{Name} to create in RT" ); 1517 $self->_show_group_info( %args ); 1518 } 1519} 1520 1521sub _show_group_info { 1522 my $self = shift; 1523 my %args = @_; 1524 my $group = $args{group}; 1525 my $rt_group = $args{rt_group}; 1526 1527 $RT::Logger->debug( "\tRT Field\tRT Value -> LDAP Value" ); 1528 foreach my $key (sort keys %$group) { 1529 my $old_value; 1530 if ($rt_group) { 1531 eval { $old_value = $rt_group->$key() }; 1532 if ($group->{$key} && defined $old_value && $old_value eq $group->{$key}) { 1533 $old_value = 'unchanged'; 1534 } 1535 } 1536 $old_value ||= 'unset'; 1537 $RT::Logger->debug( "\t$key\t$old_value => $group->{$key}" ); 1538 } 1539} 1540 1541 1542=head3 disconnect_ldap 1543 1544Disconnects from the LDAP server. 1545 1546Takes no arguments, returns nothing. 1547 1548=cut 1549 1550sub disconnect_ldap { 1551 my $self = shift; 1552 my $ldap = $self->_ldap; 1553 return unless $ldap; 1554 1555 $ldap->unbind; 1556 $ldap->disconnect; 1557 $self->_ldap(undef); 1558 return; 1559} 1560 1561RT::Base->_ImportOverlays(); 1562 15631; 1564