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

3
# XXX TODO: For historical reasons, this should become 0.33 after 0.32_xx dev releases are done.
Thomas Sibley's avatar
Thomas Sibley committed
4
our $VERSION = '0.32_02';
Kevin Falcone's avatar
Kevin Falcone committed
5
6
7
8

use warnings;
use strict;
use base qw(Class::Accessor);
9
__PACKAGE__->mk_accessors(qw(_ldap _group screendebug _users));
Kevin Falcone's avatar
Kevin Falcone committed
10
11
use Carp;
use Net::LDAP;
12
use Net::LDAP::Util qw(escape_filter_value);
Thomas Sibley's avatar
Thomas Sibley committed
13
14
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED);
Kevin Falcone's avatar
Kevin Falcone committed
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
use Data::Dumper;

=head1 NAME

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


=head1 SYNOPSIS

    use RT::Extension::LDAPImport;

=head1 METHODS

=head2 connect_ldap

Relies on the config variables $RT::LDAPHost,
$RT::LDAPUser and $RT::LDAPPassword being set
in your RT Config files.

 Set(LDAPHost,'my.ldap.host')
 Set(LDAPUSER,'me');
 Set(LDAPPassword,'mypass');

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

LDAPHost can be a hostname or an ldap:// ldaps:// uri

=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;

}

74
=head2 run_user_search
Kevin Falcone's avatar
Kevin Falcone committed
75

76
Set up the appropriate arguments for a listing of users
Kevin Falcone's avatar
Kevin Falcone committed
77

78
79
80
81
82
83
84
85
86
87
88
89
90
91
=cut

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

}

=head2 _run_search

Executes a search using the provided base and filter
Kevin Falcone's avatar
Kevin Falcone committed
92
93
94

Will connect to LDAP server using connect_ldap

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

Kevin Falcone's avatar
Kevin Falcone committed
98
99
=cut

100
sub _run_search {
Kevin Falcone's avatar
Kevin Falcone committed
101
102
    my $self = shift;
    my $ldap = $self->_ldap||$self->connect_ldap;
103
    my %args = @_;
Kevin Falcone's avatar
Kevin Falcone committed
104
105
106
107
108
109

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

Thomas Sibley's avatar
Thomas Sibley committed
110
111
112
113
114
    my %search = (
        base    => $args{base},
        filter  => $args{filter},
    );
    my (@results, $page, $cookie);
Kevin Falcone's avatar
Kevin Falcone committed
115

Thomas Sibley's avatar
Thomas Sibley committed
116
117
118
119
    if ($RT::LDAPSizeLimit) {
        $page = Net::LDAP::Control::Paged->new( size => $RT::LDAPSizeLimit, critical => 1 );
        $search{control} = $page;
    }
Kevin Falcone's avatar
Kevin Falcone committed
120

Thomas Sibley's avatar
Thomas Sibley committed
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
    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
149
150
    }

Thomas Sibley's avatar
Thomas Sibley committed
151
152
153
154
155
156
157
    # 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
158

Thomas Sibley's avatar
Thomas Sibley committed
159
160
    $self->_debug("search found ".scalar @results." objects");
    return @results;
Kevin Falcone's avatar
Kevin Falcone committed
161
162
}

163
=head2 import_users import => 1|0
Kevin Falcone's avatar
Kevin Falcone committed
164
165
166
167
168
169

Takes the results of the search from run_search
and maps attributes from LDAP into RT::User attributes
using $RT::LDAPMapping.
Creates RT users if they don't already exist.

170
171
172
With no arguments, only prints debugging information.
Pass import => 1 to actually change data.

173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
RT::LDAPMapping should be set in your RT_SiteConfig
file and looks like this.

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

RTUserField is the name of a field on an RT::User object
LDAPField can be a simple scalar and that attribute
will be looked up in LDAP.  

It can also be an arrayref, in which case each of the 
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.

193
194
195
By default users are created as Unprivileged, but you can change this by
setting $LDAPCreatePrivileged to 1.

Kevin Falcone's avatar
Kevin Falcone committed
196
197
198
199
=cut

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

Thomas Sibley's avatar
Thomas Sibley committed
202
203
    my @results = $self->run_user_search;
    unless ( @results ) {
Kevin Falcone's avatar
Kevin Falcone committed
204
205
206
207
208
        $self->_debug("No results found, no import");
        $self->disconnect_ldap;
        return;
    }

209
210
    my $mapping = $RT::LDAPMapping;
    return unless $self->_check_ldap_mapping( mapping => $mapping );
211

212
    $self->_users({});
213

Thomas Sibley's avatar
Thomas Sibley committed
214
215
    my $done = 0; my $count = scalar @results;
    while (my $entry = shift @results) {
216
        my $user = $self->_build_user_object( ldap_entry => $entry );
217
218
219
220
        unless ( $user->{Name} ) {
            $self->_warn("No Name or Emailaddress for user, skipping ".Dumper $user);
            next;
        }
221
222
223
224
        if ( $user->{Name} =~ /^[0-9]+$/) {
            $self->_warn("Skipping user '$user->{Name}', as it is numeric");
            next;
        }
225
        $self->_import_user( user => $user, ldap_entry => $entry, import => $args{import} );
226
227
        $done++;
        $self->_debug("Imported $done/$count users");
228
    }
229
    return 1;
230
231
}

232
233
234
235
236
237
238
239
240
241
=head2 _import_user

The user has run us with --import, so bring data in

=cut

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

242
    $self->_debug("Processing user $args{user}{Name}");
243
    $self->_cache_user( %args );
244

245
246
    $args{user} = $self->create_rt_user( %args );
    return unless $args{user};
247

248
249
    $self->add_user_to_group( %args );
    $self->add_custom_field_value( %args );
250

251
    return 1;
252
253
}

254
255
256
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
=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} );

    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};
}

286
287
288
289
290
291
292
293
294
295
296
297
298
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
299
            if ($user->{$key} && defined $old_value && $old_value eq $user->{$key}) {
300
301
302
303
304
305
306
                $old_value = 'unchanged';
            }
        }
        $old_value ||= 'unset';
        print "\t$key\t$old_value => $user->{$key}\n";
    }
    #$self->_debug(Dumper($user));
307
308
}

Kevin Falcone's avatar
Kevin Falcone committed
309
310
311
312
313
314
315
316
=head2 _check_ldap_mapping

Returns true is there is an LDAPMapping configured,
returns false, logs an error and disconnects from
ldap if there is no mapping.

=cut

317
318
sub _check_ldap_mapping {
    my $self = shift;
319
320
    my %args = @_;
    my $mapping = $args{mapping};
321

322
    my @rtfields = keys %{$mapping};
323
    unless ( @rtfields ) {
324
        $self->_error("No mapping found, can't import");
Kevin Falcone's avatar
Kevin Falcone committed
325
326
327
328
        $self->disconnect_ldap;
        return;
    }

329
330
331
    return 1;
}

332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
=head2 _build_user_object

Utility method which wraps _build_object to provide sane defaults for building
users.  It also tries to ensure a Name exists in the returned object.

=cut

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

350
=head2 _build_object
Kevin Falcone's avatar
Kevin Falcone committed
351

352
Builds up data from LDAP for importing
353
Returns a hash of user or group data ready for RT::User::Create or RT::Group::Create
Kevin Falcone's avatar
Kevin Falcone committed
354
355
356

=cut

357
sub _build_object {
358
359
    my $self = shift;
    my %args = @_;
360
    my $mapping = $args{mapping};
361

362
    my $object = {};
363
364
365
    foreach my $rtfield ( keys %{$mapping} ) {
        next if $rtfield =~ $args{skip};
        my $ldap_attribute = $mapping->{$rtfield};
366
367
368
369

        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
370
371
            next;
        }
372
373
        my @values;
        foreach my $attribute (@attributes) {
Kevin Falcone's avatar
Kevin Falcone committed
374
            #$self->_debug("fetching value for $attribute and storing it in $rtfield");
375
376
377
            # 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);
378
        }
379
        $object->{$rtfield} = join(' ',grep {defined} @values);
Kevin Falcone's avatar
Kevin Falcone committed
380
381
    }

382
    return $object;
Kevin Falcone's avatar
Kevin Falcone committed
383
384
}

385
=head3 _parse_ldap_mapping
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403

Internal helper function for import_user
If we're passed an arrayref, it will recurse 
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 {
404
405
    my $self = shift;
    my $mapping = shift;
406
407

    if (ref $mapping eq 'ARRAY') {
408
        return map { $self->_parse_ldap_mapping($_) } @$mapping;
409
    } elsif (ref $mapping eq 'CODE') {
410
411
412
        return map { $self->_parse_ldap_mapping($_) } $mapping->()
    } elsif (ref $mapping) {
        $self->_error("Invalid type of LDAPMapping [$mapping]");
413
414
        return;
    } else {
415
        return $mapping;
416
417
418
    }
}

Kevin Falcone's avatar
Kevin Falcone committed
419
420
421
422
423
424
425
=head2 create_rt_user

Takes a hashref of args to pass to RT::User::Create
Will try loading the user and will only create a new
user if it can't find an existing user with the Name
or EmailAddress arg passed in.

426
427
428
429
If the $LDAPUpdateUsers variable is true, data in RT
will be clobbered with data in LDAP.  Otherwise we
will skip to the next user.

430
431
432
If $LDAPUpdateOnly is true, we will not create new users
but we will update existing ones.

Kevin Falcone's avatar
Kevin Falcone committed
433
434
435
436
437
438
439
=cut

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

440
    my $user_obj = $self->_load_rt_user(%args);
Kevin Falcone's avatar
Kevin Falcone committed
441
442

    if ($user_obj->Id) {
443
        my $message = "User $user->{Name} already exists as ".$user_obj->Id;
444
        if ($RT::LDAPUpdateUsers || $RT::LDAPUpdateOnly) {
445
            $self->_debug("$message, updating their data");
446
447
448
449
450
451
452
            if ($args{import}) {
                my @results = $user_obj->Update( ARGSRef => $user, AttributesRef => [keys %$user] );
                $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 );
            }
453
454
455
        } else {
            $self->_debug("$message, skipping");
        }
456
    } else {
457
458
        if ( $RT::LDAPUpdateOnly ) {
            $self->_debug("User $user->{Name} doesn't exist in RT, skipping");
Kevin Falcone's avatar
Kevin Falcone committed
459
            return;
460
        } else {
461
            if ($args{import}) {
462
                my ($val, $msg) = $user_obj->Create( %$user, Privileged => $RT::LDAPCreatePrivileged ? 1 : 0 );
463
464
465
466
467
468
469
470
471

                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 );
472
473
                return;
            }
Kevin Falcone's avatar
Kevin Falcone committed
474
475
476
477
478
479
        }
    }

    unless ($user_obj->Id) {
        $self->_error("We couldn't find or create $user->{Name}. This should never happen");
    }
480
481
482
483
    return $user_obj;

}

484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
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;
}

499
500
501
502
=head2 add_user_to_group

Adds new users to the group specified in the $LDAPGroupName
variable (defaults to 'Imported from LDAP')
503
You can avoid this if you set $LDAPSkipAutogeneratedGroup
504
505
506
507
508
509
510
511

=cut

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

512
513
    return if $RT::LDAPSkipAutogeneratedGroup;

514
515
516
517
518
519
520
521
522
    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;
    }

523
524
525
526
527
528
529
530
    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;
531
    } else {
532
533
        $self->_debug("Would add to ".$group->Name);
        return;
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
    }
}

=head2 setup_group

Pulls the $LDAPGroupName object out of the DB or
creates it if we ened to do so.

=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
558
559
}

Kevin Falcone's avatar
Kevin Falcone committed
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
=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.

This could probably use some caching

=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;
        }

610
611
612
613
614
615
616
        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
617
        } else {
618
            $self->_debug("Would add '$cfv_name' to Custom Field '$cf_name'");
Kevin Falcone's avatar
Kevin Falcone committed
619
620
621
622
623
624
        }
    }

    return;

}
Kevin Falcone's avatar
Kevin Falcone committed
625

Kevin Falcone's avatar
Kevin Falcone committed
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
=head2 import_groups import => 1|0

Takes the results of the search from run_group_search
and maps attributes from LDAP into RT::Group attributes
using $RT::LDAPGroupMapping.

Creates groups if they don't exist

Removes users from groups if they have been removed from the group on LDAP

With no arguments, only prints debugging information.
Pass import => 1 to actually change data.

=cut

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

Thomas Sibley's avatar
Thomas Sibley committed
645
646
    my @results = $self->run_group_search;
    unless ( @results ) {
Kevin Falcone's avatar
Kevin Falcone committed
647
648
649
650
651
        $self->_debug("No results found, no group import");
        $self->disconnect_ldap;
        return;
    }

Kevin Falcone's avatar
Kevin Falcone committed
652
653
    my $mapping = $RT::LDAPGroupMapping;
    return unless $self->_check_ldap_mapping( mapping => $mapping );
Kevin Falcone's avatar
Kevin Falcone committed
654

Thomas Sibley's avatar
Thomas Sibley committed
655
656
    my $done = 0; my $count = scalar @results;
    while (my $entry = shift @results) {
Kevin Falcone's avatar
Kevin Falcone committed
657
        my $group = $self->_build_object( ldap_entry => $entry, skip => qr/(?i)^Member_Attr/, mapping => $mapping );
Kevin Falcone's avatar
Kevin Falcone committed
658
659
660
        $group->{Description} ||= 'Imported from LDAP';
        unless ( $group->{Name} ) {
            $self->_warn("No Name for group, skipping ".Dumper $group);
Kevin Falcone's avatar
Kevin Falcone committed
661
662
            next;
        }
663
664
665
666
        if ( $group->{Name} =~ /^[0-9]+$/) {
            $self->_warn("Skipping group '$group->{Name}', as it is numeric");
            next;
        }
667
        $self->_import_group( %args, group => $group, ldap_entry => $entry );
668
669
        $done++;
        $self->_debug("Imported $done/$count groups");
Kevin Falcone's avatar
Kevin Falcone committed
670
    }
Kevin Falcone's avatar
Kevin Falcone committed
671
    return 1;
Kevin Falcone's avatar
Kevin Falcone committed
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
}

=head3 run_group_search

Set up the approviate arguments for a listing of users

=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
695
696
697
698
699
700
701
702
703
704
705
706
707
=head2 _import_group

The user has run us with --import, so bring data in

=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}");
708
    my ($group_obj, $created) = $self->create_rt_group( %args, group => $group );
709
    return if $args{import} and not $group_obj;
710
    $self->add_group_members( %args, name => $group->{Name}, group => $group_obj, ldap_entry => $ldap_entry, new => $created );
Kevin Falcone's avatar
Kevin Falcone committed
711
712
713
714
715
    return;
}

=head2 create_rt_group

716
Takes a hashref of args to pass to RT::Group::Create
Kevin Falcone's avatar
Kevin Falcone committed
717
718
719
720
721
722
723
Will try loading the group and will only create a new
group if it can't find an existing group with the Name
or EmailAddress arg passed in.

If $LDAPUpdateOnly is true, we will not create new groups
but we will update existing ones.

724
725
726
There is currently no way to prevent Group data from being
clobbered from LDAP.

Kevin Falcone's avatar
Kevin Falcone committed
727
728
729
730
731
732
733
734
735
736
=cut

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

    my $group_obj = RT::Group->new($RT::SystemUser);
    $group_obj->LoadUserDefinedGroup( $group->{Name} );

737
    my $created;
Kevin Falcone's avatar
Kevin Falcone committed
738
    if ($group_obj->Id) {
739
        if ($args{import}) {
740
            $self->_debug("Group $group->{Name} already exists as ".$group_obj->Id.", updating their data");
741
742
            my @results = $group_obj->Update( ARGSRef => $group, AttributesRef => [keys %$group] );
            $self->_debug(join("\n",@results)||'no change');
743
        } else {
744
745
            print "Found existing group $group->{Name} to update\n";
            $self->_show_group_info( %args, rt_group => $group_obj );
746
        }
747
    } else {
748
749
750
751
        if ( $RT::LDAPUpdateOnly ) {
            $self->_debug("Group $group->{Name} doesn't exist in RT, skipping");
            return;
        }
Kevin Falcone's avatar
Kevin Falcone committed
752

753
754
755
756
757
758
        if ($args{import}) {
            my ($val, $msg) = $group_obj->CreateUserDefinedGroup( %$group );
            unless ($val) {
                $self->_error("couldn't create group_obj for $group->{Name}: $msg");
                return;
            }
759
            $created = $val;
760
761
762
763
            $self->_debug("Created group for $group->{Name} with id ".$group_obj->Id);
        } else {
            print "Found new group $group->{Name} to create in RT\n";
            $self->_show_group_info( %args );
Kevin Falcone's avatar
Kevin Falcone committed
764
765
766
767
768
769
770
            return;
        }
    }

    unless ($group_obj->Id) {
        $self->_error("We couldn't find or create $group->{Name}. This should never happen");
    }
771
    return ($group_obj, $created);
Kevin Falcone's avatar
Kevin Falcone committed
772
773
774

}

Kevin Falcone's avatar
Kevin Falcone committed
775
776
=head3 add_group_members

777
Iterate over the list of values in the Member_Attr LDAP entry.
Kevin Falcone's avatar
Kevin Falcone committed
778
779
780
781
782
783
784
Look up the appropriate username from LDAP.
Add those users to the group.
Remove members of the RT Group who are no longer members
of the LDAP group.

=cut

Kevin Falcone's avatar
Kevin Falcone committed
785
786
787
788
sub add_group_members {
    my $self = shift;
    my %args = @_;
    my $group = $args{group};
789
    my $groupname = $args{name};
Kevin Falcone's avatar
Kevin Falcone committed
790
791
    my $ldap_entry = $args{ldap_entry};

792
793
    $self->_debug("Processing group membership for $groupname");

794
    my $members = $self->_get_group_members_from_ldap(%args);
Kevin Falcone's avatar
Kevin Falcone committed
795
796

    unless (defined $members) {
Kevin Falcone's avatar
Kevin Falcone committed
797
        $self->_warn("No members found for $groupname in Member_Attr");
Kevin Falcone's avatar
Kevin Falcone committed
798
799
800
        return;
    }

801
    my %rt_group_members;
802
    if ($args{group} and not $args{new}) {
803
        my $user_members = $group->UserMembersObj( Recursively => 0);
804
        while ( my $member = $user_members->Next ) {
805
            $rt_group_members{$member->Name} = $member;
806
807
808
        }
    } elsif (not $args{import}) {
        $self->_debug("No group in RT, would create with members:");
809
810
    }

811
    my $users = $self->_users;
Kevin Falcone's avatar
Kevin Falcone committed
812
    foreach my $member (@$members) {
813
        my $username;
814
815
        if (exists $users->{lc $member}) {
            next unless $username = $users->{lc $member};
816
        } else {
Thomas Sibley's avatar
Thomas Sibley committed
817
            my @results = $self->_run_search(
818
819
820
                base   => $RT::LDAPBase,
                filter => "(&$RT::LDAPFilter($RT::LDAPGroupMapping->{Member_Attr_Value}="
                            . escape_filter_value($member) . "))"
821
            );
Thomas Sibley's avatar
Thomas Sibley committed
822
            unless ( @results ) {
823
                $users->{lc $member} = undef;
824
825
826
                $self->_error("No user found for $member who should be a member of $groupname");
                next;
            }
Thomas Sibley's avatar
Thomas Sibley committed
827
            my $ldap_user = shift @results;
828
            $username = $self->_cache_user( ldap_entry => $ldap_user );
Kevin Falcone's avatar
Kevin Falcone committed
829
        }
830
        if ( delete $rt_group_members{$username} ) {
831
            $self->_debug("\t$username\tin RT and LDAP");
832
833
            next;
        }
834
835
836
        $self->_debug($group ? "\t$username\tin LDAP, adding to RT" : "\t$username");
        next unless $args{import};

Kevin Falcone's avatar
Kevin Falcone committed
837
838
839
840
841
842
843
844
        my $rt_user = RT::User->new($RT::SystemUser);
        my ($res,$msg) = $rt_user->Load( $username );
        unless ($res) {
            $self->_warn("Unable to load $username: $msg");
            next;
        }
        ($res,$msg) = $group->AddMember($rt_user->PrincipalObj->Id);
        unless ($res) {
Kevin Falcone's avatar
Kevin Falcone committed
845
            $self->_warn("Failed to add $username to $groupname: $msg");
Kevin Falcone's avatar
Kevin Falcone committed
846
847
848
        }
    }

849
    for my $username (sort keys %rt_group_members) {
850
851
852
        $self->_debug("\t$username\tin RT, not in LDAP, removing");
        next unless $args{import};

853
        my ($res,$msg) = $group->DeleteMember($rt_group_members{$username}->PrincipalObj->Id);
854
855
856
857
        unless ($res) {
            $self->_warn("Failed to remove $username to $groupname: $msg");
        }
    }
Kevin Falcone's avatar
Kevin Falcone committed
858
859
}

860
861
862
863
864
865
866
867
868
869
870
sub _get_group_members_from_ldap {
    my $self = shift;
    my %args = @_;
    my $ldap_entry = $args{ldap_entry};

    my $mapping = $RT::LDAPGroupMapping;

    my $members = $ldap_entry->get_value($mapping->{Member_Attr}, asref => 1);
}


Kevin Falcone's avatar
Kevin Falcone committed
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
=head2 _show_group

Show debugging information about the group record we're going to import
when the groups reruns us with --import

=cut

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

    my $rt_group = RT::Group->new($RT::SystemUser);
    $rt_group->LoadUserDefinedGroup( $group->{Name} );

    if ( $rt_group->Id ) {
        print "Found existing group $group->{Name} to update\n";
        $self->_show_group_info( %args, rt_group => $rt_group );
    } else {
        print "Found new group $group->{Name} to create in RT\n";
        $self->_show_group_info( %args );
    }
}
Kevin Falcone's avatar
Kevin Falcone committed
894

Kevin Falcone's avatar
Kevin Falcone committed
895
896
897
898
899
900
901
902
903
904
905
906
907
sub _show_group_info {
    my $self = shift;
    my %args = @_;
    my $group = $args{group};
    my $rt_group = $args{rt_group};

    return unless $self->screendebug;

    print "\tRT Field\tRT Value -> LDAP Value\n";
    foreach my $key (sort keys %$group) {
        my $old_value;
        if ($rt_group) {
            eval { $old_value = $rt_group->$key() };
Thomas Sibley's avatar
Thomas Sibley committed
908
            if ($group->{$key} && defined $old_value && $old_value eq $group->{$key}) {
Kevin Falcone's avatar
Kevin Falcone committed
909
910
911
912
913
914
                $old_value = 'unchanged';
            }
        }
        $old_value ||= 'unset';
        print "\t$key\t$old_value => $group->{$key}\n";
    }
Kevin Falcone's avatar
Kevin Falcone committed
915
916
}

Kevin Falcone's avatar
Kevin Falcone committed
917

Kevin Falcone's avatar
Kevin Falcone committed
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
=head3 disconnect_ldap

Disconnects from the LDAP server

Takes no arguments, returns nothing

=cut

sub disconnect_ldap {
    my $self = shift;
    my $ldap = $self->_ldap;
    return unless $ldap;

    $ldap->unbind;
    $ldap->disconnect;
    return;
}

936
937
=head1 Utility Functions

Kevin Falcone's avatar
Kevin Falcone committed
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
=head3 screendebug

We always log to the RT log file with level debug 

This duplicates the messages to the screen

=cut

sub _debug {
    my $self = shift;
    my $msg  = shift;

    $RT::Logger->debug($msg);

    return unless $self->screendebug;
    print $msg, "\n";

}

sub _error {
    my $self = shift;
    my $msg  = shift;

    $RT::Logger->error($msg);
    print STDERR $msg, "\n";
}

sub _warn {
    my $self = shift;
    my $msg  = shift;

969
    $RT::Logger->warning($msg);
Kevin Falcone's avatar
Kevin Falcone committed
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
    print STDERR $msg, "\n";
}

=head1 BUGS AND LIMITATIONS

No bugs have been reported.

Please report any bugs or feature requests to
C<bug-rt-extension-ldapimport@rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org>.


=head1 AUTHOR

Kevin Falcone  C<< <falcone@bestpractical.com> >>


=head1 LICENCE AND COPYRIGHT

Copyright (c) 2007, Best Practical Solutions, LLC.  All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.


=head1 DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
For faster browsing, not all history is shown. View entire blame