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