LDAPImport.pm 37.8 KB
Newer Older
Kevin Falcone's avatar
Kevin Falcone committed
1
2
package RT::Extension::LDAPImport;

Thomas Sibley's avatar
Thomas Sibley committed
3
our $VERSION = '0.33_02';
Kevin Falcone's avatar
Kevin Falcone committed
4
5
6
7

use warnings;
use strict;
use base qw(Class::Accessor);
8
__PACKAGE__->mk_accessors(qw(_ldap _group screendebug _users));
Kevin Falcone's avatar
Kevin Falcone committed
9
10
use Carp;
use Net::LDAP;
11
use Net::LDAP::Util qw(escape_filter_value);
Thomas Sibley's avatar
Thomas Sibley committed
12
13
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED);
Kevin Falcone's avatar
Kevin Falcone committed
14
15
16
17
18
19
20
21
use Data::Dumper;

=head1 NAME

RT::Extension::LDAPImport - Import Users from an LDAP store

=head1 SYNOPSIS

22
In C<RT_SiteConfig.pm>:
Jim Brandt's avatar
Jim Brandt committed
23
24

    Set($LDAPHost,'my.ldap.host')
25
    Set($LDAPUser,'me');
Jim Brandt's avatar
Jim Brandt committed
26
27
28
29
30
31
32
    Set($LDAPPassword,'mypass');
    Set($LDAPFilter, '(&(cn = users))');
    Set($LDAPMapping, {Name         => 'uid', # required
                       EmailAddress => 'mail',
                       RealName     => 'cn',
                       WorkPhone    => 'telephoneNumber',
                       Organization => 'departmentName'});
33
    
Jim Brandt's avatar
Jim Brandt committed
34
35
    # Add to any existing plugins
    Set(@Plugins, qw(RT::Extension::LDAPImport));
36
    
Jim Brandt's avatar
Jim Brandt committed
37
    # If you want to sync Groups RT <-> LDAP
38
    
Jim Brandt's avatar
Jim Brandt committed
39
40
41
42
43
44
    Set($LDAPGroupBase, 'ou=Groups,o=Our Place');
    Set($LDAPGroupFilter, '(&(cn = Groups))');
    Set($LDAPGroupMapping, {Name               => 'cn',
                            Member_Attr        => 'member',
                            Member_Attr_Value  => 'dn' });

45
46
Running the import:

Jim Brandt's avatar
Jim Brandt committed
47
48
49
    # Run a test import
    /opt/rt4/local/plugins/RT-Extension-LDAPImport/bin/rtldapimport \
    --debug > ldapimport.debug 2>&1
50
    
Jim Brandt's avatar
Jim Brandt committed
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
    # Run for real, possibly put in cron
    /opt/rt4/local/plugins/RT-Extension-LDAPImport/bin/rtldapimport \
    --import

=head1 CONFIGURATION

All of the configuration for the importer goes
your C<RT_SiteConfig.pm> file. Some of these values pass through
to L<Net::LDAP> so you can check there for valid values and more
advanced options.

=over

=item C<< Set($LDAPHost,'our.ldap.host'); >>

Hostname or ldap(s):// uri:

=item C<< Set($LDAPUser, 'uid=foo,ou=users,dc=example,dc=com'); >>

Your LDAP username or DN. If unset, we'll attempt an anonymous bind.

=item C<< Set($LDAPPassword, 'ldap pass'); >>

Your LDAP password.

=item C<< Set($LDAPBase, 'ou=People,o=Our Place'); >>

Base object to search from.

=item C<< Set($LDAPFilter, '(&(cn = users))'); >>

The LDAP search filter to apply (in this case, find all the users).

=item C<< Set($LDAPMapping... >>

    Set($LDAPMapping, {Name         => 'uid',
                       EmailAddress => 'mail',
                       RealName     => 'cn',
                       WorkPhone    => 'telephoneNumber',
                       Organization => 'departmentName'});

This provides the mapping of attributes in RT to attribute in LDAP.
Only Name is required for RT.

The LDAP attributes can also be an arrayref of LDAP fields

    WorkPhone => [qw/CompanyPhone Extension/]

which will be concatenated together with a space.

The LDAP attribute can also be a subroutine reference
that returns either an arrayref or a list of attributes.

104
105
106
107
108
109
110
111
112
113
114
115
116
117
The keys in the mapping (i.e. the RT fields, the left hand side) may be a user
custom field name prefixed with C<UserCF.>, for example C<< 'UserCF.Employee
Number' => 'employeeId' >>.  Note that this only B<adds> values at the moment,
which on single value CFs will remove any old value first.  Multiple value CFs
may behave not quite how you expect.  If the attribute no longer exists on a
user in LDAP, it will be cleared on the RT side as well.

You may also prefix any RT custom field name with C<CF.> inside your mapping to
add available values to a Select custom field.  This effectively takes user
attributes in LDAP and adds the values as selectable options in a CF.  It does
B<not> set a CF value on any RT object (User, Ticket, Queue, etc).  You might
use this to populate a ticket Location CF with all the locations of your users
so that tickets can be associated with the locations in use.

Jim Brandt's avatar
Jim Brandt committed
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
=item C<< Set($LDAPCreatePrivileged, 1); >>

By default users are created as Unprivileged, but you can change this by
setting C<$LDAPCreatePrivileged> to 1.

=item C<< Set($LDAPGroupName,'My Imported Users'); >>

The RT Group new and updated users belong to. By default, all users
added or updated by the importer will belong to the 'Imported from LDAP'
group.

=item C<< Set($LDAPSkipAutogeneratedGroup, 1); >>

Set this to true to prevent users from being automatically
added to the group configured by C<$LDAPGroupName>.

=item C<< Set($LDAPUpdateUsers, 1); >>

By default, existing users are skipped.  If you
turn on LDAPUpdateUsers, we will clobber existing
data with data from LDAP.

=item C<< Set($LDAPUpdateOnly, 1); >>

By default, we create users who don't exist in RT but do
match your LDAP filter and obey C<$LDAPUpdateUsers> for existing
users.  This setting updates existing users, overriding
C<$LDAPUpdateUsers>, but won't create new
users who are found in LDAP but not in RT.

=item C<< Set($LDAPGroupBase, 'ou=Groups,o=Our Place'); >>

Where to search for groups to import.

=item C<< Set($LDAPGroupFilter, '(&(cn = Groups))'); >>

The search filter to apply.

=item C<< Set($LDAPGroupMapping... >>

    Set($LDAPGroupMapping, {Name               => 'cn',
                            Member_Attr        => 'member',
                            Member_Attr_Value  => 'dn' });

A mapping of RT attributes to LDAP attributes to identify group members.
Name will become the name of the group in RT, in this case pulling
from the cn attribute on the LDAP group record returned.

C<Member_Attr> is the field in the LDAP group record the importer should
look at for group members. These values (there may be multiple members)
will then be compared to the RT user name, which came from the LDAP
user record.

C<Member_Attr_Value>, which defaults to 'dn', specifies where on the LDAP
user record the importer should look to compare the member value.
A match between the member field on the group record and this
identifier (dn or other LDAP field) on a user record means the
user will be added to that group in RT.

You can provide a C<Description> key which will be added as the group
description in RT. The default description is 'Imported from LDAP'.

=item C<< Set($LDAPSizeLimit, 1000); >>

You can set this value if your LDAP server has result size limits.

=back

=head1 Mapping Groups Between RT and LDAP

If you are using the importer, you likely want to manage access via
LDAP by putting people in groups like 'DBAs' and 'IT Support', but
also have groups for other non-RT related things. In this case, you
won't want to create all of your LDAP groups in RT. To limit the groups
that get mirrored, construct your C<$LDAPGroupFilter> as an OR (|) with
all of the RT groups you want to mirror from LDAP. For example:

    Set($LDAPGroupBase, 'OU=Groups,OU=Company,DC=COM');
    Set($LDAPGroupFilter, '(|(CN=DBAs)(CN=IT Support))');

The importer will then import only the groups that match. In this case,
import means:

=over

=item * Verifying the group is in AD;

=item * Creating the group in RT if it doesn't exist;

=item * Populating the group with the members identified in AD;

=back

The import script will also issue a warning if a user isn't found in RT,
but this should only happen when testing. When running with --import on,
users are created before groups are processed, so all users (group
members) should exist unless there are inconsistencies in your LDAP configuration.

=head1 Running the Import

Executing C<rtldapimport> will run a test that connects to your LDAP server
and prints out a list of the users found. To see more about these users,
and to see more general debug information, include the C<--debug> flag.

That debug information is also sent to the RT log with the debug level.
Errors are logged to the screen and to the RT log.

Executing C<rtldapimport> with the C<--import> flag will cause it to import
users into your RT database. It is recommended that you make a database
backup before doing this. If your filters aren't set properly this could
create a lot of users or groups in your RT instance.

=head1 RT Versions

The importer works with RT 3.8 and newer including RT 4.

It may work with RT 3.6.

=head1 LDAP Filters

The L<ldapsearch|http://www.openldap.org/software/man.cgi?query=ldapsearch&manpath=OpenLDAP+2.0-Release>
utility in openldap can be very helpful while refining your filters.
Kevin Falcone's avatar
Kevin Falcone committed
240
241
242
243
244

=head1 METHODS

=head2 connect_ldap

Jim Brandt's avatar
Jim Brandt committed
245
246
Relies on the config variables C<$RT::LDAPHost>,
C<$RT::LDAPUser> and C<$RT::LDAPPassword> being set
Kevin Falcone's avatar
Kevin Falcone committed
247
248
in your RT Config files.

Jim Brandt's avatar
Jim Brandt committed
249
250
251
 Set($LDAPHost,'my.ldap.host')
 Set($LDAPUSER,'me');
 Set($LDAPPassword,'mypass');
Kevin Falcone's avatar
Kevin Falcone committed
252
253
254
255

LDAPUser and LDAPPassword can be blank,
which will cause an anonymous bind.

Jim Brandt's avatar
Jim Brandt committed
256
LDAPHost can be a hostname or an ldap:// ldaps:// uri.
Kevin Falcone's avatar
Kevin Falcone committed
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288

=cut

sub connect_ldap {
    my $self = shift;

    my $ldap = Net::LDAP->new($RT::LDAPHost);
    $self->_debug("connecting to $RT::LDAPHost");
    unless ($ldap) {
        $self->_error("Can't connect to $RT::LDAPHost");
        return;
    }

    my $msg;
    if ($RT::LDAPUser) {
        $self->_debug("binding as $RT::LDAPUser");
        $msg = $ldap->bind($RT::LDAPUser, password => $RT::LDAPPassword);
    } else {
        $self->_debug("binding anonymously");
        $msg = $ldap->bind;
    }

    if ($msg->code) {
        $self->_error("LDAP bind failed " . $msg->error);
        return;
    }

    $self->_ldap($ldap);
    return $ldap;

}

289
=head2 run_user_search
Kevin Falcone's avatar
Kevin Falcone committed
290

Jim Brandt's avatar
Jim Brandt committed
291
Set up the appropriate arguments for a listing of users.
Kevin Falcone's avatar
Kevin Falcone committed
292

293
294
295
296
297
298
299
300
301
302
303
304
305
=cut

sub run_user_search {
    my $self = shift;
    $self->_run_search(
        base   => $RT::LDAPBase,
        filter => $RT::LDAPFilter
    );

}

=head2 _run_search

Jim Brandt's avatar
Jim Brandt committed
306
Executes a search using the provided base and filter.
Kevin Falcone's avatar
Kevin Falcone committed
307

Jim Brandt's avatar
Jim Brandt committed
308
Will connect to LDAP server using C<connect_ldap>.
Kevin Falcone's avatar
Kevin Falcone committed
309

Thomas Sibley's avatar
Thomas Sibley committed
310
311
312
Returns an array of L<Net::LDAP::Entry> objects, possibly consolidated from
multiple LDAP pages.

Kevin Falcone's avatar
Kevin Falcone committed
313
314
=cut

315
sub _run_search {
Kevin Falcone's avatar
Kevin Falcone committed
316
317
    my $self = shift;
    my $ldap = $self->_ldap||$self->connect_ldap;
318
    my %args = @_;
Kevin Falcone's avatar
Kevin Falcone committed
319
320
321
322
323
324

    unless ($ldap) {
        $self->_error("fetching an LDAP connection failed");
        return;
    }

Thomas Sibley's avatar
Thomas Sibley committed
325
326
327
328
329
    my %search = (
        base    => $args{base},
        filter  => $args{filter},
    );
    my (@results, $page, $cookie);
Kevin Falcone's avatar
Kevin Falcone committed
330

Thomas Sibley's avatar
Thomas Sibley committed
331
332
333
334
    if ($RT::LDAPSizeLimit) {
        $page = Net::LDAP::Control::Paged->new( size => $RT::LDAPSizeLimit, critical => 1 );
        $search{control} = $page;
    }
Kevin Falcone's avatar
Kevin Falcone committed
335

Thomas Sibley's avatar
Thomas Sibley committed
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
    LOOP: {
        # Start where we left off
        $page->cookie($cookie) if $page and $cookie;

        $self->_debug("searching with: " . join(' ', map { "$_ => '$search{$_}'" } sort keys %search));

        my $result = $ldap->search( %search );

        if ($result->code) {
            $self->_error("LDAP search failed " . $result->error);
            last;
        }

        push @results, $result->entries;

        # Short circuit early if we're done
        last if not $result->count
             or $result->count < ($RT::LDAPSizeLimit || 0);

        if ($page) {
            if (my $control = $result->control( LDAP_CONTROL_PAGED )) {
                $cookie = $control->cookie;
            } else {
                $self->_error("LDAP search didn't return a paging control");
                last;
            }
        }
        redo if $cookie;
Kevin Falcone's avatar
Kevin Falcone committed
364
365
    }

Thomas Sibley's avatar
Thomas Sibley committed
366
367
368
369
370
371
372
    # Let the server know we're abandoning the search if we errored out
    if ($cookie) {
        $self->_debug("Informing the LDAP server we're done with the result set");
        $page->cookie($cookie);
        $page->size(0);
        $ldap->search( %search );
    }
Kevin Falcone's avatar
Kevin Falcone committed
373

Thomas Sibley's avatar
Thomas Sibley committed
374
375
    $self->_debug("search found ".scalar @results." objects");
    return @results;
Kevin Falcone's avatar
Kevin Falcone committed
376
377
}

378
=head2 import_users import => 1|0
Kevin Falcone's avatar
Kevin Falcone committed
379
380

Takes the results of the search from run_search
Jim Brandt's avatar
Jim Brandt committed
381
382
and maps attributes from LDAP into C<RT::User> attributes
using C<$RT::LDAPMapping>.
Kevin Falcone's avatar
Kevin Falcone committed
383
384
Creates RT users if they don't already exist.

385
With no arguments, only prints debugging information.
Jim Brandt's avatar
Jim Brandt committed
386
Pass C<--import> to actually change data.
387

Jim Brandt's avatar
Jim Brandt committed
388
389
C<$RT::LDAPMapping>> should be set in your C<RT_SiteConfig.pm>
file and look like this.
390
391
392

 Set($LDAPMapping, { RTUserField => LDAPField, RTUserField => LDAPField });

Jim Brandt's avatar
Jim Brandt committed
393
RTUserField is the name of a field on an C<RT::User> object
394
LDAPField can be a simple scalar and that attribute
Jim Brandt's avatar
Jim Brandt committed
395
will be looked up in LDAP.
396

Jim Brandt's avatar
Jim Brandt committed
397
It can also be an arrayref, in which case each of the
398
399
400
401
402
403
404
405
406
407
elements will be evaluated in turn.  Scalars will be
looked up in LDAP and concatenated together with a single
space.

If the value is a sub reference, it will be executed.
The sub should return a scalar, which will be examined.
If it is a scalar, the value will be looked up in LDAP.
If it is an arrayref, the values will be concatenated 
together with a single space.

408
By default users are created as Unprivileged, but you can change this by
Jim Brandt's avatar
Jim Brandt committed
409
setting C<$LDAPCreatePrivileged> to 1.
410

Kevin Falcone's avatar
Kevin Falcone committed
411
412
413
414
=cut

sub import_users {
    my $self = shift;
415
    my %args = @_;
Kevin Falcone's avatar
Kevin Falcone committed
416

Thomas Sibley's avatar
Thomas Sibley committed
417
418
    my @results = $self->run_user_search;
    unless ( @results ) {
Kevin Falcone's avatar
Kevin Falcone committed
419
420
421
422
423
        $self->_debug("No results found, no import");
        $self->disconnect_ldap;
        return;
    }

424
425
    my $mapping = $RT::LDAPMapping;
    return unless $self->_check_ldap_mapping( mapping => $mapping );
426

427
    $self->_users({});
428

Thomas Sibley's avatar
Thomas Sibley committed
429
430
    my $done = 0; my $count = scalar @results;
    while (my $entry = shift @results) {
431
        my $user = $self->_build_user_object( ldap_entry => $entry );
432
433
434
435
        unless ( $user->{Name} ) {
            $self->_warn("No Name or Emailaddress for user, skipping ".Dumper $user);
            next;
        }
436
        if ( $user->{Name} =~ /^[0-9]+$/) {
437
            $self->_debug("Skipping user '$user->{Name}', as it is numeric");
438
439
            next;
        }
440
        $self->_import_user( user => $user, ldap_entry => $entry, import => $args{import} );
441
442
        $done++;
        $self->_debug("Imported $done/$count users");
443
    }
444
    return 1;
445
446
}

447
448
=head2 _import_user

Jim Brandt's avatar
Jim Brandt committed
449
The user has run us with --import, so bring data in.
450
451
452
453
454
455
456

=cut

sub _import_user {
    my $self = shift;
    my %args = @_;

457
    $self->_debug("Processing user $args{user}{Name}");
458
    $self->_cache_user( %args );
459

460
461
    $args{user} = $self->create_rt_user( %args );
    return unless $args{user};
462

463
464
    $self->add_user_to_group( %args );
    $self->add_custom_field_value( %args );
465
    $self->update_object_custom_field_values( %args, object => $args{user} );
466

467
    return 1;
468
469
}

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
=head2 _cache_user ldap_entry => Net::LDAP::Entry, [user => { ... }]

Adds the user to a global cache which is used when importing groups later.

Optionally takes a second argument which is a user data object returned by
_build_user_object.  If not given, _cache_user will call _build_user_object
itself.

Returns the user Name.

=cut

sub _cache_user {
    my $self = shift;
    my %args = (@_);
    my $user = $args{user} || $self->_build_user_object( ldap_entry => $args{ldap_entry} );

487
488
    $self->_users({}) if not defined $self->_users;

489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
    my $group_map       = $RT::LDAPGroupMapping           || {};
    my $member_attr_val = $group_map->{Member_Attr_Value} || 'dn';
    my $membership_key  = lc $member_attr_val eq 'dn'
                            ? $args{ldap_entry}->dn
                            : $args{ldap_entry}->get_value($member_attr_val);

    # Fallback to the DN if the user record doesn't have a value
    unless (defined $membership_key) {
        $membership_key = $args{ldap_entry}->dn;
        $self->_warn("User attribute '$member_attr_val' has no value for '$membership_key'; falling back to DN");
    }

    return $self->_users->{lc $membership_key} = $user->{Name};
}

504
505
506
507
508
509
510
511
512
513
514
515
516
sub _show_user_info {
    my $self = shift;
    my %args = @_;
    my $user = $args{user};
    my $rt_user = $args{rt_user};

    return unless $self->screendebug;

    print "\tRT Field\tRT Value -> LDAP Value\n";
    foreach my $key (sort keys %$user) {
        my $old_value;
        if ($rt_user) {
            eval { $old_value = $rt_user->$key() };
Thomas Sibley's avatar
Thomas Sibley committed
517
            if ($user->{$key} && defined $old_value && $old_value eq $user->{$key}) {
518
519
520
521
522
523
524
                $old_value = 'unchanged';
            }
        }
        $old_value ||= 'unset';
        print "\t$key\t$old_value => $user->{$key}\n";
    }
    #$self->_debug(Dumper($user));
525
526
}

Kevin Falcone's avatar
Kevin Falcone committed
527
528
=head2 _check_ldap_mapping

Jim Brandt's avatar
Jim Brandt committed
529
Returns true is there is an C<LDAPMapping> configured,
Kevin Falcone's avatar
Kevin Falcone committed
530
531
532
533
534
returns false, logs an error and disconnects from
ldap if there is no mapping.

=cut

535
536
sub _check_ldap_mapping {
    my $self = shift;
537
538
    my %args = @_;
    my $mapping = $args{mapping};
539

540
    my @rtfields = keys %{$mapping};
541
    unless ( @rtfields ) {
542
        $self->_error("No mapping found, can't import");
Kevin Falcone's avatar
Kevin Falcone committed
543
544
545
546
        $self->disconnect_ldap;
        return;
    }

547
548
549
    return 1;
}

550
551
=head2 _build_user_object

Jim Brandt's avatar
Jim Brandt committed
552
553
554
Utility method which wraps C<_build_object> to provide sane
defaults for building users.  It also tries to ensure a Name
exists in the returned object.
555
556
557
558
559
560

=cut

sub _build_user_object {
    my $self = shift;
    my $user = $self->_build_object(
561
        skip    => qr/(?i)^(?:User)?CF\./,
562
563
564
565
566
567
568
        mapping => $RT::LDAPMapping,
        @_
    );
    $user->{Name} ||= $user->{EmailAddress};
    return $user;
}

569
=head2 _build_object
Kevin Falcone's avatar
Kevin Falcone committed
570

571
Builds up data from LDAP for importing
Jim Brandt's avatar
Jim Brandt committed
572
573
Returns a hash of user or group data ready for
C<RT::User::Create> or C<RT::Group::Create>.
Kevin Falcone's avatar
Kevin Falcone committed
574
575
576

=cut

577
sub _build_object {
578
579
    my $self = shift;
    my %args = @_;
580
    my $mapping = $args{mapping};
581

582
    my $object = {};
583
584
585
    foreach my $rtfield ( keys %{$mapping} ) {
        next if $rtfield =~ $args{skip};
        my $ldap_attribute = $mapping->{$rtfield};
586
587
588
589

        my @attributes = $self->_parse_ldap_mapping($ldap_attribute);
        unless (@attributes) {
            $self->_error("Invalid LDAP mapping for $rtfield ".Dumper($ldap_attribute));
Kevin Falcone's avatar
Kevin Falcone committed
590
591
            next;
        }
592
593
        my @values;
        foreach my $attribute (@attributes) {
Kevin Falcone's avatar
Kevin Falcone committed
594
            #$self->_debug("fetching value for $attribute and storing it in $rtfield");
595
596
597
            # otherwise we'll pull 7 alternate names out of the Name field
            # this may want to be configurable
            push @values, scalar $args{ldap_entry}->get_value($attribute);
598
        }
599
        $object->{$rtfield} = join(' ',grep {defined} @values);
Kevin Falcone's avatar
Kevin Falcone committed
600
601
    }

602
    return $object;
Kevin Falcone's avatar
Kevin Falcone committed
603
604
}

605
=head3 _parse_ldap_mapping
606

Jim Brandt's avatar
Jim Brandt committed
607
608
Internal helper function for C<import_user>.
If we're passed an arrayref, it will recurse
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
over each of the elements in case one of them is
another arrayref or subroutine.

If we're passed a subref, it executes the code
and recurses over each of the returned values
so that a returned array or arrayref will work.

If we're passed a scalar, returns that.

Returns a list of values that need to be concatenated
together.

=cut

sub _parse_ldap_mapping {
624
625
    my $self = shift;
    my $mapping = shift;
626
627

    if (ref $mapping eq 'ARRAY') {
628
        return map { $self->_parse_ldap_mapping($_) } @$mapping;
629
    } elsif (ref $mapping eq 'CODE') {
630
631
632
        return map { $self->_parse_ldap_mapping($_) } $mapping->()
    } elsif (ref $mapping) {
        $self->_error("Invalid type of LDAPMapping [$mapping]");
633
634
        return;
    } else {
635
        return $mapping;
636
637
638
    }
}

Kevin Falcone's avatar
Kevin Falcone committed
639
640
=head2 create_rt_user

Jim Brandt's avatar
Jim Brandt committed
641
Takes a hashref of args to pass to C<RT::User::Create>
Kevin Falcone's avatar
Kevin Falcone committed
642
Will try loading the user and will only create a new
Jim Brandt's avatar
Jim Brandt committed
643
644
user if it can't find an existing user with the C<Name>
or C<EmailAddress> arg passed in.
Kevin Falcone's avatar
Kevin Falcone committed
645

Jim Brandt's avatar
Jim Brandt committed
646
If the C<$LDAPUpdateUsers> variable is true, data in RT
647
648
649
will be clobbered with data in LDAP.  Otherwise we
will skip to the next user.

Jim Brandt's avatar
Jim Brandt committed
650
If C<$LDAPUpdateOnly> is true, we will not create new users
651
652
but we will update existing ones.

Kevin Falcone's avatar
Kevin Falcone committed
653
654
655
656
657
658
659
=cut

sub create_rt_user {
    my $self = shift;
    my %args = @_;
    my $user = $args{user};

660
    my $user_obj = $self->_load_rt_user(%args);
Kevin Falcone's avatar
Kevin Falcone committed
661
662

    if ($user_obj->Id) {
663
        my $message = "User $user->{Name} already exists as ".$user_obj->Id;
664
        if ($RT::LDAPUpdateUsers || $RT::LDAPUpdateOnly) {
665
            $self->_debug("$message, updating their data");
666
            if ($args{import}) {
667
                my @results = $user_obj->Update( ARGSRef => $user, AttributesRef => [keys %$user] );
668
669
670
671
672
                $self->_debug(join("\n",@results)||'no change');
            } else {
                $self->_debug("Found existing user $user->{Name} to update");
                $self->_show_user_info( %args, rt_user => $user_obj );
            }
673
674
675
        } else {
            $self->_debug("$message, skipping");
        }
676
    } else {
677
678
        if ( $RT::LDAPUpdateOnly ) {
            $self->_debug("User $user->{Name} doesn't exist in RT, skipping");
Kevin Falcone's avatar
Kevin Falcone committed
679
            return;
680
        } else {
681
            if ($args{import}) {
682
                my ($val, $msg) = $user_obj->Create( %$user, Privileged => $RT::LDAPCreatePrivileged ? 1 : 0 );
683
684
685
686
687
688
689
690
691

                unless ($val) {
                    $self->_error("couldn't create user_obj for $user->{Name}: $msg");
                    return;
                }
                $self->_debug("Created user for $user->{Name} with id ".$user_obj->Id);
            } else {
                print "Found new user $user->{Name} to create in RT\n";
                $self->_show_user_info( %args );
692
693
                return;
            }
Kevin Falcone's avatar
Kevin Falcone committed
694
695
696
697
698
699
        }
    }

    unless ($user_obj->Id) {
        $self->_error("We couldn't find or create $user->{Name}. This should never happen");
    }
700
701
702
703
    return $user_obj;

}

704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
sub _load_rt_user {
    my $self = shift;
    my %args = @_;
    my $user = $args{user};

    my $user_obj = RT::User->new($RT::SystemUser);

    $user_obj->Load( $user->{Name} );
    unless ($user_obj->Id) {
        $user_obj->LoadByEmail( $user->{EmailAddress} );
    }

    return $user_obj;
}

719
720
=head2 add_user_to_group

Jim Brandt's avatar
Jim Brandt committed
721
722
723
Adds new users to the group specified in the C<$LDAPGroupName>
variable (defaults to 'Imported from LDAP').
You can avoid this if you set C<$LDAPSkipAutogeneratedGroup>.
724
725
726
727
728
729
730
731

=cut

sub add_user_to_group {
    my $self = shift;
    my %args = @_;
    my $user = $args{user};

732
733
    return if $RT::LDAPSkipAutogeneratedGroup;

734
735
736
737
738
739
740
741
742
    my $group = $self->_group||$self->setup_group;

    my $principal = $user->PrincipalObj;

    if ($group->HasMember($principal)) {
        $self->_debug($user->Name . " already a member of " . $group->Name);
        return;
    }

743
744
745
746
747
748
749
750
    if ($args{import}) {
        my ($status, $msg) = $group->AddMember($principal->Id);
        if ($status) {
            $self->_debug("Added ".$user->Name." to ".$group->Name." [$msg]");
        } else {
            $self->_error("Couldn't add ".$user->Name." to ".$group->Name." [$msg]");
        }
        return $status;
751
    } else {
752
753
        $self->_debug("Would add to ".$group->Name);
        return;
754
755
756
757
758
    }
}

=head2 setup_group

Jim Brandt's avatar
Jim Brandt committed
759
760
Pulls the C<$LDAPGroupName> object out of the DB or
creates it if we need to do so.
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777

=cut

sub setup_group  {
    my $self = shift;
    my $group_name = $RT::LDAPGroupName||'Imported from LDAP';
    my $group = RT::Group->new($RT::SystemUser);

    $group->LoadUserDefinedGroup( $group_name );
    unless ($group->Id) {
        my ($id,$msg) = $group->CreateUserDefinedGroup( Name => $group_name );
        unless ($id) {
            $self->_error("Can't create group $group_name [$msg]")
        }
    }

    $self->_group($group);
Kevin Falcone's avatar
Kevin Falcone committed
778
779
}

Kevin Falcone's avatar
Kevin Falcone committed
780
781
782
783
784
785
=head3 add_custom_field_value

Adds values to a Select (one|many) Custom Field.
The Custom Field should already exist, otherwise
this will throw an error and not import any data.

Jim Brandt's avatar
Jim Brandt committed
786
This could probably use some caching.
Kevin Falcone's avatar
Kevin Falcone committed
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829

=cut

sub add_custom_field_value {
    my $self = shift;
    my %args = @_;
    my $user = $args{user};

    foreach my $rtfield ( keys %{$RT::LDAPMapping} ) {
        next unless $rtfield =~ /^CF\.(.+)$/i;
        my $cf_name = $1;
        my $ldap_attribute = $RT::LDAPMapping->{$rtfield};

        my @attributes = $self->_parse_ldap_mapping($ldap_attribute);
        unless (@attributes) {
            $self->_error("Invalid LDAP mapping for $rtfield ".Dumper($ldap_attribute));
            next;
        }
        my @values;
        foreach my $attribute (@attributes) {
            #$self->_debug("fetching value for $attribute and storing it in $rtfield");
            # otherwise we'll pull 7 alternate names out of the Name field
            # this may want to be configurable
            push @values, scalar $args{ldap_entry}->get_value($attribute);
        }
        my $cfv_name = join(' ',@values); 
        next unless $cfv_name;

        my $cf = RT::CustomField->new($RT::SystemUser);
        my ($status, $msg) = $cf->Load($cf_name);
        unless ($status) {
            $self->_error("Couldn't load CF [$cf_name]: $msg");
            next;
        }

        my $cfv = RT::CustomFieldValue->new($RT::SystemUser);
        $cfv->LoadByCols( CustomField => $cf->id, 
                          Name => $cfv_name );
        if ($cfv->id) {
            $self->_debug("Custom Field '$cf_name' already has '$cfv_name' for a value");
            next;
        }

830
831
832
833
834
835
836
        if ($args{import}) {
            ($status, $msg) = $cf->AddValue( Name => $cfv_name );
            if ($status) {
                $self->_debug("Added '$cfv_name' to Custom Field '$cf_name' [$msg]");
            } else {
                $self->_error("Couldn't add '$cfv_name' to '$cf_name' [$msg]");
            }
Kevin Falcone's avatar
Kevin Falcone committed
837
        } else {
838
            $self->_debug("Would add '$cfv_name' to Custom Field '$cf_name'");
Kevin Falcone's avatar
Kevin Falcone committed
839
840
841
842
843
844
        }
    }

    return;

}
Kevin Falcone's avatar
Kevin Falcone committed
845

846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
=head3 update_object_custom_field_values

Adds CF values to an object (currently only users).  The Custom Field should
already exist, otherwise this will throw an error and not import any data.

Note that this code only B<adds> values at the moment, which on single value
CFs will remove any old value first.  Multiple value CFs may behave not quite
how you expect.

=cut

sub update_object_custom_field_values {
    my $self = shift;
    my %args = @_;
    my $obj  = $args{object};

    foreach my $rtfield ( keys %{$RT::LDAPMapping} ) {
        # XXX TODO: accept GroupCF when we call this from group_import too
        next unless $rtfield =~ /^UserCF\.(.+)$/i;
        my $cf_name = $1;
        my $ldap_attribute = $RT::LDAPMapping->{$rtfield};

        my @attributes = $self->_parse_ldap_mapping($ldap_attribute);
        unless (@attributes) {
            $self->_error("Invalid LDAP mapping for $rtfield ".Dumper($ldap_attribute));
            next;
        }
        my $value = join ' ',
                    grep { defined and length }
                     map { scalar $args{ldap_entry}->get_value($_) }
                         @attributes;

878
879
880
881
882
883
884
        my $current = $obj->FirstCustomFieldValue($cf_name);

        if (not defined $current and not defined $value) {
            $self->_debug($obj->Name . ": Skipping '$cf_name'.  No value in RT or LDAP.");
            next;
        }
        elsif (defined $current and defined $value and $current eq $value) {
885
886
887
888
889
890
891
892
893
894
895
896
897
            $self->_debug($obj->Name . ": Value '$value' is already set for '$cf_name'");
            next;
        }

        $self->_debug($obj->Name . ": Adding object value '$value' for '$cf_name'");
        next unless $args{import};

        my ($ok, $msg) = $obj->AddCustomFieldValue( Field => $cf_name, Value => $value );
        $self->_error($obj->Name . ": Couldn't add value '$value' for '$cf_name': $msg")
            unless $ok;
    }
}

Kevin Falcone's avatar
Kevin Falcone committed
898
899
=head2 import_groups import => 1|0

Jim Brandt's avatar
Jim Brandt committed
900
901
902
Takes the results of the search from C<run_group_search>
and maps attributes from LDAP into C<RT::Group> attributes
using C<$RT::LDAPGroupMapping>.
Kevin Falcone's avatar
Kevin Falcone committed
903

Jim Brandt's avatar
Jim Brandt committed
904
Creates groups if they don't exist.
Kevin Falcone's avatar
Kevin Falcone committed
905

Jim Brandt's avatar
Jim Brandt committed
906
Removes users from groups if they have been removed from the group on LDAP.
Kevin Falcone's avatar
Kevin Falcone committed
907
908

With no arguments, only prints debugging information.
Jim Brandt's avatar
Jim Brandt committed
909
Pass C<--import> to actually change data.
Kevin Falcone's avatar
Kevin Falcone committed
910
911
912
913
914
915
916

=cut

sub import_groups {
    my $self = shift;
    my %args = @_;

Thomas Sibley's avatar
Thomas Sibley committed
917
918
    my @results = $self->run_group_search;
    unless ( @results ) {
Kevin Falcone's avatar
Kevin Falcone committed
919
920
921
922
923
        $self->_debug("No results found, no group import");
        $self->disconnect_ldap;
        return;
    }

Kevin Falcone's avatar
Kevin Falcone committed
924
925
    my $mapping = $RT::LDAPGroupMapping;
    return unless $self->_check_ldap_mapping( mapping => $mapping );
Kevin Falcone's avatar
Kevin Falcone committed
926

Thomas Sibley's avatar
Thomas Sibley committed
927
928
    my $done = 0; my $count = scalar @results;
    while (my $entry = shift @results) {
Kevin Falcone's avatar
Kevin Falcone committed
929
        my $group = $self->_build_object( ldap_entry => $entry, skip => qr/(?i)^Member_Attr/, mapping => $mapping );
Kevin Falcone's avatar
Kevin Falcone committed
930
931
932
        $group->{Description} ||= 'Imported from LDAP';
        unless ( $group->{Name} ) {
            $self->_warn("No Name for group, skipping ".Dumper $group);
Kevin Falcone's avatar
Kevin Falcone committed
933
934
            next;
        }
935
        if ( $group->{Name} =~ /^[0-9]+$/) {
936
            $self->_debug("Skipping group '$group->{Name}', as it is numeric");
937
938
            next;
        }
939
        $self->_import_group( %args, group => $group, ldap_entry => $entry );
940
941
        $done++;
        $self->_debug("Imported $done/$count groups");
Kevin Falcone's avatar
Kevin Falcone committed
942
    }
Kevin Falcone's avatar
Kevin Falcone committed
943
    return 1;
Kevin Falcone's avatar
Kevin Falcone committed
944
945
946
947
}

=head3 run_group_search

Jim Brandt's avatar
Jim Brandt committed
948
Set up the appropriate arguments for a listing of users.
Kevin Falcone's avatar
Kevin Falcone committed
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966

=cut

sub run_group_search {
    my $self = shift;

    unless ($RT::LDAPGroupBase && $RT::LDAPGroupFilter) {
        $self->_warn("Not running a group import, configuration not set");
        return;
    }
    $self->_run_search(
        base   => $RT::LDAPGroupBase,
        filter => $RT::LDAPGroupFilter
    );

}


Kevin Falcone's avatar
Kevin Falcone committed
967
968
=head2 _import_group

Jim Brandt's avatar
Jim Brandt committed
969
The user has run us with C<--import>, so bring data in.
Kevin Falcone's avatar
Kevin Falcone committed
970
971
972
973
974
975
976
977
978
979

=cut

sub _import_group {
    my $self = shift;
    my %args = @_;
    my $group = $args{group};
    my $ldap_entry = $args{ldap_entry};

    $self->_debug("Processing group $group->{Name}");
980
    my ($group_obj, $created) = $self->create_rt_group( %args, group => $group );
981
    return if $args{import} and not $group_obj;
982
    $self->add_group_members( %args, name => $group->{Name}, group => $group_obj, ldap_entry => $ldap_entry, new => $created );
983
    # XXX TODO: support OCFVs for groups too
Kevin Falcone's avatar
Kevin Falcone committed
984
985
986
987
988
    return;
}

=head2 create_rt_group

Jim Brandt's avatar
Jim Brandt committed
989
Takes a hashref of args to pass to C<RT::Group::Create>
Kevin Falcone's avatar
Kevin Falcone committed
990
Will try loading the group and will only create a new
Jim Brandt's avatar
Jim Brandt committed
991
992
group if it can't find an existing group with the C<Name>
or C<EmailAddress> arg passed in.
Kevin Falcone's avatar
Kevin Falcone committed
993

Jim Brandt's avatar
Jim Brandt committed
994
If C<$LDAPUpdateOnly> is true, we will not create new groups
Kevin Falcone's avatar
Kevin Falcone committed
995
996
but we will update existing ones.

997
998
999
There is currently no way to prevent Group data from being
clobbered from LDAP.

Kevin Falcone's avatar
Kevin Falcone committed
1000
=cut
For faster browsing, not all history is shown. View entire blame