source: shelldap @ c6a3abc56c74

Revision c6a3abc56c74, 39.2 KB checked in by Mahlon E. Smith <mahlon@…>, 8 months ago (diff)

Fix bug introduced in rev:a3a710f720dd with passwd arguments.

  • Property exe set to *
Line 
1#!/usr/bin/env perl
2# vim: set nosta noet ts=4 sw=4:
3#
4# Copyright (c) 2006-2011, Mahlon E. Smith <mahlon@martini.nu>
5# All rights reserved.
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#
9#     * Redistributions of source code must retain the above copyright
10#       notice, this list of conditions and the following disclaimer.
11#
12#     * Redistributions in binary form must reproduce the above copyright
13#       notice, this list of conditions and the following disclaimer in the
14#       documentation and/or other materials provided with the distribution.
15#
16#     * Neither the name of Mahlon E. Smith nor the names of his
17#       contributors may be used to endorse or promote products derived
18#       from this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
21# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23# DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
24# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31=head1 NAME
32
33Shelldap - A program for interacting with an LDAP server via a shell-like interface
34
35=head1 DESCRIPTION
36
37Shelldap /LDAP::Shell is a program for interacting with an LDAP server via a shell-like
38interface.
39
40This is not meant to be an exhaustive LDAP editing and browsing
41interface, but rather an intuitive shell for performing basic LDAP
42tasks quickly and with minimal effort.
43
44=head1 SYNPOSIS
45
46 shelldap --server example.net [--help]
47
48=head1 FEATURES
49
50 - Upon successful authenticated binding, credential information is
51   auto-cached to ~/.shelldap.rc -- future loads require no command line
52   flags.
53
54 - Custom 'description maps' for entry listings.  (See the 'list' command.)
55
56 - History and autocomplete via readline, if installed.
57
58 - Automatic reconnection attempts if the connection is lost with the
59   LDAP server.
60
61 - It feels like a semi-crippled shell, making LDAP browsing and editing
62   at least halfway pleasurable.
63
64=head1 OPTIONS
65
66All command line options follow getopts long conventions.
67
68    shelldap --server example.net --basedn dc=your,o=company
69
70You may also optionally create a ~/.shelldap.rc file with command line
71defaults.  This file should be valid YAML.  (This file is generated
72automatically on a successful bind auth.)
73
74Example:
75
76    server: ldap.example.net
77    binddn: cn=Manager,dc=your,o=company
78    bindpass: xxxxxxxxx
79    basedn: dc=your,o=company
80    tls: yes
81    tls_cacert: /etc/ssl/certs/cacert.pem
82    tls_cert:   ~/.ssl/client.cert.pem
83    tls_key:    ~/.ssl/private/client.key.pem
84
85=over 4
86
87=item B<server>
88
89Required. The LDAP server to connect to.  This can be a hostname, IP
90address, or a URI.
91
92    --server ldaps://ldap.example.net
93    -H ldaps://ldap.example.net
94
95=back
96
97=over 4
98
99=item B<binddn>
100
101The full dn of a user to authenticate as.  If not specified, defaults to
102an anonymous bind.  You will be prompted for a password.
103
104    --binddn cn=Manager,dc=your,o=company
105    -D cn=Manager,dc=your,o=company
106
107=back
108
109=over 4
110
111=item B<basedn>
112
113The directory 'root' of your LDAP server.  If omitted, shelldap will
114try and ask the server for a sane default.
115
116    --basedn dc=your,o=company
117    -b dc=your,o=company
118
119=back
120
121=over 4
122
123=item B< tls>
124
125Enables TLS over what would normally be an insecure connection.
126Requires server side support.
127
128=item B<tls_cacert>
129
130Specify CA Certificate to trust.
131
132    --tls_cacert /etc/ssl/certs/cacert.pem
133
134=item B<tls_cert>
135
136The TLS client certificate.
137
138    --tls_cert ~/.ssl/client.cert.pem
139
140=item B<tls_key>
141
142The TLS client key.  Not specifying a key will connect via TLS without
143key verification.
144
145    --tls_key ~/.ssl/private/client.key.pem
146
147=back
148
149=over 4
150
151=item B<cacheage>
152
153Set the time to cache directory lookups in seconds.
154
155By default, directory lookups are cached for 300 seconds, to speed
156autocomplete up when changing between different basedns.
157
158Modifications to the directory automatically reset the cache.  Directory
159listings are not cached.  (This is just used for autocomplete.)  Set it
160to 0 to disable caching completely.
161
162=back
163
164=over 4
165
166=item B<timeout>
167
168Set the maximum time an LDAP operation can take before it is cancelled.
169
170=back
171
172=over 4
173
174=item B<debug>
175
176Print extra operational info out, and backtrace on fatal error.
177
178=back
179
180=over 4
181
182=item B<version>
183
184Display the version number.
185
186=back
187
188=head1 SHELL COMMANDS
189
190=over 4
191
192=item B< cat>
193
194Display an LDIF dump of an entry.  Globbing is supported.  Specify
195either the full dn, or an rdn.  For most commands, rdns are local to the
196current search base. ('cwd', as translated to shell speak.)  You may additionally
197add a list of attributes to display.  Use '+' for server side attributes.
198
199    cat uid=mahlon
200    cat ou=*
201    cat uid=mahlon,ou=People,dc=example,o=company
202    cat uid=mahlon + userPassword
203
204=item B<  cd>
205
206Change directory.  Translated to LDAP, this changes the current basedn.
207All commands after a 'cd' operate within the new basedn.
208
209    cd                  change to 'home' basedn
210    cd ~                change to the binddn, or basedn if anonymously bound
211    cd -                change to previous node
212    cd ou=People        change to explicit path below current node
213    cd ..               change to parent node
214    cd ../../ou=Groups  change to node ou=Groups, which is a sibling
215                        to the current node's grandparent
216
217Since LDAP doesn't actually limit what can be a container object, you
218can actually cd into any entry. Many commands then work on '.', meaning
219"wherever I currently am."
220
221    cd uid=mahlon
222    cat .
223
224=item B<clear>
225
226Clear the screen.
227
228=item B<copy>
229
230Copy an entry to a different dn path.  All copies are relative to the
231current basedn, unless a full dn is specified.  All attributes are
232copied, then an LDAP moddn() is performed.
233
234    copy uid=mahlon uid=bob
235    copy uid=mahlon ou=Others,dc=example,o=company
236    copy uid=mahlon,ou=People,dc=example,o=company uid=mahlon,ou=Others,dc=example,o=company
237
238aliased to: cp
239
240=item B<create>
241
242Create an entry from scratch.  Arguments are space separated objectClass
243names.  Possible objectClasses are derived automatically from the
244server, and will tab-complete.
245
246After the classes are specified, an editor will launch.  Required
247attributes are listed first, then optional attributes.  Optionals are
248commented out.  After the editor exits, the resulting LDIF is validated
249and added to the LDAP directory.
250
251    create top person organizationalPerson inetOrgPerson posixAccount
252
253aliased to: touch
254
255=item B<delete>
256
257Remove an entry from the directory.  Globbing is supported.
258All deletes are sanity-prompted.
259
260    delete uid=mahlon
261    delete uid=ma*
262
263aliased to: rm
264
265=item B<edit>
266
267Edit an entry in an external editor.  After the editor exits, the
268resulting LDIF is sanity checked, and changes are written to the LDAP
269directory.
270
271    edit uid=mahlon
272
273aliased to: vi
274
275=item B< env>
276
277 Show values for various runtime variables.
278
279=item B<grep>
280
281Search for arbitrary LDAP filters, and return matching dn results.
282The search string must be a valid LDAP filter.
283
284    grep uid=mahlon
285    grep uid=mahlon ou=People
286    grep -r (&(uid=mahlon)(objectClass=*))
287
288 aliased to: search
289
290=item B<list>
291
292List entries for the current basedn.  Globbing is supported.
293
294aliased to: ls
295
296    ls -l
297    ls -lR uid=mahlon
298    list uid=m*
299
300In 'long' mode, descriptions are listed as well, if they exist.
301There are some default 'long listing' mappings for common objectClass
302types.  You can additionally specify your own mappings in your
303.shelldap.rc, like so:
304
305    ...
306    descmaps:
307        objectClass: attributename
308        posixAccount: gecos
309        posixGroup: gidNumber
310        ipHost: ipHostNumber
311
312=item B<mkdir>
313
314Creates a new 'organizationalUnit' entry.
315
316    mkdir containername
317    mkdir ou=whatever
318
319=item B<move>
320
321Move an entry to a different dn path.  Usage is identical to B<copy>.
322
323aliased to: mv
324
325=item B<passwd>
326
327If supported server side, change the password for a specified entry.
328The entry must have a 'userPassword' attribute.
329
330    passwd uid=mahlon
331
332=item B< pwd>
333
334Print the 'working directory' - aka, the current ldap basedn.
335
336=item B<setenv>
337
338Modify various runtime variables normally set from the command line.
339
340    setenv debug 1
341    export debug=1
342
343=item B<whoami>
344
345Show current auth credentials.  Unless you specified a binddn, this
346will just show an anonymous bind.
347
348=back
349
350=head1 TODO
351
352Referral support.  Currently, if you try to write to a replicant slave,
353you'll just get a referral.  It would be nice if shelldap automatically
354tried to follow it.
355
356For now, it only makes sense to connect to a master if you plan on doing
357any writes.
358
359=head1 BUGS / LIMITATIONS
360
361There is no support for editing binary data.  If you need to edit base64
362stuff, just feed it to the regular ldapmodify/ldapadd/etc tools.
363
364=head1 AUTHOR
365
366Mahlon E. Smith <mahlon@martini.nu>
367
368=cut
369
370package LDAP::Shell;
371use strict;
372use warnings;
373use Term::ReadKey;
374use Term::Shell;
375use Digest::MD5;
376use Net::LDAP qw/ LDAP_SUCCESS LDAP_SERVER_DOWN /;
377use Net::LDAP::Util qw/ canonical_dn ldap_explode_dn /;
378use Net::LDAP::LDIF;
379use Data::Dumper;
380use File::Temp;
381use Algorithm::Diff;
382use Carp 'confess';
383use base 'Term::Shell';
384require Net::LDAP::Extension::SetPassword;
385
386my $conf = $main::conf;
387
388# make 'die' backtrace in debug mode
389$SIG{'__DIE__'} = \&Carp::confess if $conf->{'debug'};
390
391###############################################################
392#
393# UTILITY FUNCTIONS
394#
395###############################################################
396
397# initial shell behaviors
398#
399sub init
400{
401    my $self = shift;
402    $self->{'API'}->{'match_uniq'} = 0;
403
404    $self->{'editor'} = $ENV{'EDITOR'} || 'vi';
405    $self->{'env'}  = [ qw/ debug cacheage timeout / ];
406
407    # let autocomplete work with the '=' character
408    my $term = $self->term();
409    $term->Attribs->{'basic_word_break_characters'}  =~ s/=//m;
410    $term->Attribs->{'completer_word_break_characters'} =~ s/=//m;
411
412    # read in history
413    eval {
414        $term->history_truncate_file("$ENV{'HOME'}/.shelldap_history", 50);
415        $term->ReadHistory("$ENV{'HOME'}/.shelldap_history");
416    };
417
418    $self->{'root_dse'} = $self->ldap->root_dse();
419    if ( $conf->{'debug'} ) {
420        $self->{'schema'}   = $self->ldap->schema();
421        my @versions = $self->{'root_dse'}->get_value('supportedLDAPVersion');
422        print "Connected to $conf->{'server'}\n";
423        print "Supported LDAP version: ", ( join ', ', @versions ), "\n";
424        print "Cipher in use: ", $self->ldap()->cipher(), "\n";
425    }
426
427    # try an initial search and die if it doesn't work
428    # (bad baseDN)
429    my $s = $self->search();
430    die "LDAP baseDN error: ", $s->{'message'}, "\n" if $s->{'code'};
431
432    $self->{'schema'} = $self->ldap->schema();
433
434    # okay, now do an initial population of 'cwd'
435    # for autocomplete.
436    $self->update_entries();
437
438    # whew, okay.  Update prompt, wait for input!
439    $self->update_prompt();
440
441    return;
442}
443
444
445# get an ldap connection handle
446#
447sub ldap
448{
449    my $self = shift;
450
451    # use cached connection object if it exists
452    return $self->{'ldap'} if $self->{'ldap'};
453   
454    # fill in potentially missing info
455    die "No server specified.\n" unless $conf->{'server'};
456
457    # Emit a nicer error message if IO::Socket::SSL is
458    # not installed and Net::LDAP decides it is required.
459    #
460    if ( $conf->{'tls'} || $conf->{'server'} =~ m|ldaps://| ) {
461        eval 'use IO::Socket::SSL';
462        die qq{IO::Socket::SSL not installed, but is required for SSL or TLS connections.
463You may try connecting insecurely, or install the module and try again.\n} if $@;
464    }
465
466    if ( $conf->{'binddn'} && ! $conf->{'bindpass'} ) {
467        print "Bind password: ";
468        Term::ReadKey::ReadMode 2;
469        chomp($conf->{'bindpass'} = <STDIN>);
470        Term::ReadKey::ReadMode 0;
471        print "\n";
472    }
473
474    # make connection
475    my $ldap = Net::LDAP->new( $conf->{'server'} )
476        or die "Unable to connect to LDAP server '$conf->{'server'}': $!\n";
477
478    # secure connection options
479    #
480    if ( $conf->{'tls'} )  {
481        if ( $conf->{'tls_key'} ) {
482            $ldap->start_tls( 
483                verify     => 'require',
484                cafile     => $conf->{'tls_cacert'},
485                clientcert => $conf->{'tls_cert'},
486                clientkey  => $conf->{'tls_key'},
487                keydecrypt => sub {
488                    print "Key Passphrase: "; 
489                    Term::ReadKey::ReadMode 2;
490                    chomp(my $secret = <STDIN>);
491                    Term::ReadKey::ReadMode 0;
492                    print "\n";
493                    return $secret;
494                });
495        }
496        else {
497            $ldap->start_tls( verify => 'none' );
498        }
499    }
500
501    # bind
502    my $rv;
503    if ( $conf->{'binddn'} ) {
504        # authed
505        $rv = $ldap->bind(
506            $conf->{'binddn'},
507            password => $conf->{'bindpass'}
508        );
509    }
510    else {
511        # anon
512        $rv = $ldap->bind();
513    }
514
515    my $err = $rv->error();
516    if ( $rv->code() ) {
517        $err .= " (try the --tls flag?)"
518            if $err =~ /confidentiality required/i;
519        die "LDAP bind error: $err\n";
520    }
521
522    # offer to cache authentication info
523    # if we enter this conditional, we have successfully
524    # authed with the server (non anonymous), and
525    # we haven't cached anything in the past.
526    #
527    if ( $conf->{'binddn'} && ! -e $conf->{'confpath'} ) {
528        print "Would you like to cache your connection information? [Y/n]: ";
529        chomp( my $response = <STDIN> );
530        unless ( $response =~ /^n/i ) {
531            YAML::Syck::DumpFile( $conf->{'confpath'}, $conf );
532            chmod 0600, $conf->{'confpath'};
533            print "Connection info cached to $conf->{'confpath'}.\n";
534        }
535    }
536
537    $self->{'ldap'} = $ldap;
538    return $ldap;
539}
540
541# just return an LDIF object
542#
543sub ldif 
544{
545    my $self     = shift;
546    my $use_temp = shift;
547
548    # create tmpfile and link ldif object with it
549    if ( $use_temp ) {
550        my ( undef, $fname ) =
551          File::Temp::tempfile( 'shelldap_XXXXXXXX', DIR => '/tmp', UNLINK => 1 );
552        $self->{'ldif'}    = Net::LDAP::LDIF->new( $fname, 'w', sort => 1 );
553        $self->{'ldif_fname'} = $fname;
554    }
555
556    # ldif -> stdout
557    else {
558        $self->{'ldif'} = Net::LDAP::LDIF->new( \*STDOUT, 'w', sort => 1 );
559    }
560
561    return $self->{'ldif'};
562}
563
564# load and return an Entry object from LDIF
565#
566sub load_ldif
567{
568    my $self = shift;
569
570    my $ldif =  Net::LDAP::LDIF->new( shift(), 'r' );
571    return unless $ldif;
572
573    my $e;
574    eval { $e = $ldif->read_entry(); };
575
576    return if $@;
577    return $e;
578}
579
580# given a filename, return an md5 checksum
581#
582sub chksum 
583{
584    my $self = shift;
585    my $file = shift or return;
586
587    my $md5 = Digest::MD5->new();
588    open F, $file or die "Unable to read temporary ldif: $!\n";
589    my $hash = $md5->addfile( *F )->hexdigest();
590    close F;
591
592    return $hash;
593}
594
595# prompt functions
596#
597sub prompt_str
598{
599    my $self = shift;
600    return $self->{'prompt'};
601}
602sub update_prompt 
603{
604    my $self = shift;
605    my $base = $self->base();
606
607    if ( length $base > 50 ) {
608        my $cwd_dn = $1 if $base =~ /^(.*?),/;
609        $self->{'prompt'} = "... $cwd_dn > ";
610    }
611    else {
612        my $prompt = $base;
613        $prompt =~ s/$conf->{'basedn'}/~/;
614        $self->{'prompt'} = "$prompt > ";
615    }
616    return;
617}
618
619# search base accessor
620#
621sub base 
622{
623    my $self = shift;
624    $self->{'base'} ||= $conf->{'basedn'};
625
626    # try and determine base automatically from rootDSE
627    #
628    unless ( $self->{'base'} ) {
629        my @namingContexts = $self->{'root_dse'}->get_value('namingContexts');
630        $conf->{'basedn'} = $namingContexts[0];
631        $self->{'base'}   = $namingContexts[0];
632    }
633    if ( $_[0] ) {
634        my $base = canonical_dn( $_[0], casefold => 'none' );
635        $self->{'base'} = $base if $base;
636    }
637    return $self->{'base'};
638}
639
640# do a search on a dn to determine if it is valid.
641# returns a bool.
642#
643sub is_valid_dn 
644{
645    my $self = shift;
646    my $dn   = shift or return 0;
647
648    my $r = $self->search({ base => $dn });
649
650    return $r->{'code'} == LDAP_SUCCESS ? 1 : 0;
651}
652
653# perform an ldap search
654# return an hashref containing return code and
655# arrayref of Net::LDAP::Entry objects
656#
657sub search 
658{
659    my $self = shift;
660    my $opts = shift || {};
661
662    $opts->{'base'}   ||= $self->base(),
663    $opts->{'filter'} ||= '(objectClass=*)';
664    $opts->{'scope'}  ||= 'base';
665
666    my $s = $self->ldap->search(
667        base      => $opts->{'base'},
668        filter    => $opts->{'filter'},
669        scope     => $opts->{'scope'},
670        timelimit => $conf->{'timeout'},
671        typesonly => ! $opts->{'vals'},
672        attrs     => $opts->{'attrs'} || ['*']
673    );
674
675    my $rv = {
676        code      => $s->code(),
677        message   => $s->error(),
678        entries   => []
679    };
680
681    # since search is used just about everywhere, this seems like
682    # a pretty good place to check for connection errors.
683    #
684    # check for a lost connection, kill cached object so we
685    # try to reconnect on the next search.
686    #
687    $self->{'ldap'} = undef if $s->code() == LDAP_SERVER_DOWN;
688
689    $rv->{'entries'} =
690      $opts->{'scope'} eq 'base' ? [ $s->shift_entry() ] : [ $s->entries() ];
691
692    return $rv;
693}
694
695# update the autocomplete for entries
696# in the current base tree, respecting or creating cache.
697#
698sub update_entries 
699{
700    my $self = shift;
701    my %opts = @_;
702    my $base = lc( $self->base() );
703   
704    my $s = $opts{'search'} || $self->search({ scope => 'one' });
705
706    $self->{'cwd_entries'} = [];
707    return if $s->{'code'};
708
709    # setup cache object
710    $self->{'cache'} ||= {};
711    $self->{'cache'}->{ $base } ||= {};
712    $self->{'cache'}->{ $base } = {} if $opts{'clearcache'};
713    my $cache = $self->{'cache'}->{ $base };
714
715    my $now = time();
716    if ( ! exists $cache->{'entries'}
717        or $now - $cache->{'timestamp'} > $conf->{'cacheage'} )
718    {
719        $self->debug("Caching entries for $base\n");
720        foreach my $e ( @{ $s->{'entries'} } ) {
721            my $dn  = $e->dn();
722            my $rdn = $dn;
723            $rdn =~ s/,$base//i;  # remove base from display
724            push @{ $self->{'cwd_entries'} }, $rdn;
725        }
726        $cache->{'timestamp'} = $now;
727        $cache->{'entries'} = $self->{'cwd_entries'};
728    }
729    else {
730        $self->debug("Using cached lookups for $base\n");
731    }
732
733    $self->{'cwd_entries'} = $cache->{'entries'};
734    return;
735}
736
737# convert a given path to a DN: deal with '..', '.'
738# Synopsis: $dn = $self->path_to_dn( $path );
739sub path_to_dn
740{
741    my $self    = shift;
742    my $path    = shift;
743    my %flags   = @_;
744    my $curbase = $self->base();
745
746    # return current base DN
747    return $curbase if $path eq '.';
748
749    # support 'cd -'
750    return $self->{'previous_base'} if $path eq '-';
751
752    # support empty 'cd' or 'cd ~' going to root
753    return $conf->{'basedn'} if $path eq '~' || ! $path;
754
755    # relative path, upwards
756    #
757    if ( $path =~ /^\.\./o ) {
758        # support '..' (possibly iterated and as prefix to a DN)
759        my @base = @{ ldap_explode_dn($curbase, casefold => 'none') };
760
761        # deal with leading ..,
762        while ( $path =~ /^\.\./ ) {
763            shift( @base ) if @base;
764            $path =~ s/^\.\.//;
765            last if $path !~ /[,\/]\s*/;
766            $path =~ s/[,\/]\s*//;
767        }
768
769        # append the new dn to the node if one was specified:
770        #    cd ../../cn=somewhere  vs
771        #    cd ../../
772        #
773        my $newbase_root = canonical_dn( \@base, casefold => 'none' );
774        $path = $path ? $path . ',' . $newbase_root : $newbase_root;
775    }
776
777    # attach the base if it isn't already there (this takes care of
778    # deeper relative nodes and absolutes)
779    #
780    else {
781        $path = "$path," . $curbase unless $path =~ /$curbase/;
782    }
783
784    return $path;
785}
786
787# given an array ref of shell-like globs,
788# make and return an LDAP filter object.
789#
790sub make_filter 
791{
792    my $self  = shift;
793    my $globs = shift or return;
794
795    return unless ref $globs eq 'ARRAY';
796    return unless scalar @$globs;
797
798    my $filter;
799    $filter = join('', map { (/^\(.*\)$/o) ? $_ : "($_)" } @$globs);
800    $filter = '(|' . $filter . ')'  if (scalar(@$globs) > 1);
801    $filter = Net::LDAP::Filter->new($filter);
802
803    if ( $filter ) {
804        $self->debug('Filter parsed as: ' . $filter->as_string() . "\n");
805    }
806    else {
807        print "Error parsing filter.\n";
808        return;
809    }
810
811    return $filter;
812}
813
814
815# check whether a given string may be a filter
816# Synopsis: $yesNo = $self->is_valid_filter($string);
817#
818sub is_valid_filter
819{
820    my $self   = shift;
821    my $filter = shift or return;
822
823    return Net::LDAP::Filter->new( $filter ) ? 1 : 0;
824}
825
826
827# little. yellow. different. better.
828#
829sub debug 
830{
831    my $self = shift;
832    return unless $conf->{'debug'};
833    print "\e[33m";
834    print shift();
835    print "\e[0m";
836    return;
837}
838
839# setup command autocompletes for
840# all commands that have the same possible values
841#
842sub autocomplete_cwd
843{
844    my $self = shift;
845    my $word = $_[0];
846
847    return sort @{ $self->{'cwd_entries'} };
848}
849
850sub comp_setenv
851{ 
852    my $self = shift;
853    return @{ $self->{'env'} };
854}
855
856sub comp_create
857{
858    my $self = shift;
859    return @{ $self->{'objectclasses'} } if $self->{'objectclasses'};
860
861    my @oc_data = $self->{'schema'}->all_objectclasses();
862    my @oc;
863    foreach my $o ( @oc_data ) {
864        push @oc, $o->{'name'};
865    }
866    @oc = sort @oc;
867    $self->{'objectclasses'} = \@oc;
868
869    return @oc;
870}
871
872{
873    no warnings;
874    no strict 'refs';
875
876    # command, alias
877    my %cmd_map = (
878        whoami => 'id',
879        list   => 'ls',
880        grep   => 'search',
881        edit   => 'vi',
882        delete => 'rm',
883        copy   => 'cp',
884        cat    => 'read',
885        move   => 'mv',
886        cd     => undef,
887        passwd => undef
888    );
889
890    # setup autocompletes
891    foreach ( %cmd_map ) {
892        next unless $_;
893        my $sub = "comp_$_";
894        *$sub   = \&autocomplete_cwd;
895    }
896    *comp_touch  = \&comp_create;
897    *comp_export = \&comp_setenv;
898
899    # setup alias subs
900    #
901    # Term::Shell has an alias_* feature, but
902    # it seems to work about 90% of the time.
903    # that last 10% is something of a mystery.
904    #
905    $cmd_map{'create'} = 'touch';
906    foreach my $cmd ( keys %cmd_map ) {
907        next unless defined $cmd_map{$cmd};
908        my $alias_sub = 'run_' . $cmd_map{$cmd};
909        my $real_sub  = 'run_' . $cmd;
910        *$alias_sub = \&$real_sub;
911    }
912}
913
914
915# Given an $arrayref, remove LDIF continuation wrapping,
916# effectively making each entry a single line.
917#
918sub unwrap {
919    my $array = shift;
920
921    my $i = 1;
922    while ( $i < scalar(@$array) ) {
923        if ( $array->[$i] =~ /^\s/ ) {
924            $array->[ $i - 1 ] =~ s/\n$//;
925            $array->[ $i ] =~ s/^\s//;
926            splice( @$array, $i - 1, 2, $array->[$i - 1] . $array->[$i] );
927        }
928        else {
929            $i++;
930        }
931    }
932}
933
934
935###############################################################
936#
937# SHELL METHODS
938#
939###############################################################
940
941# don't die on a newline
942#
943sub run_ { return; }
944
945# print shell debug actions
946#
947sub precmd
948{
949    my $self = shift;
950    my ( $handler, $cmd, $args ) = @_;
951
952    my $term = $self->term();
953    eval { $term->WriteHistory("$ENV{'HOME'}/.shelldap_history"); };
954
955    return unless $conf->{'debug'};
956    $self->debug( "$$cmd (" . ( join ' ', @$args ) . "), calling '$$handler'\n" );
957    return;
958} 
959
960sub run_cat 
961{
962    my $self  = shift;
963    my $dn    = shift;
964    my @attrs = (@_) ? @_ : ('*');
965
966    unless ( $dn ) {
967        print "No dn provided.\n";
968        return;
969    }
970
971    # support '.'
972    $dn = $self->base() if $dn eq '.';
973
974    # support globbing
975    my $s;
976    if ( $dn eq '*' ) {
977        $s = $self->search({
978            scope  => 'one',
979            vals   => 1,
980            attrs  => \@attrs
981        });
982    }
983    elsif ( $dn =~ /\*/ ) {
984        $s = $self->search({
985            scope  => 'one',
986            vals   => 1,
987            filter => $dn,
988            attrs  => \@attrs
989        });
990    }
991    else {
992        # convert given path to DN
993        $dn = $self->path_to_dn( $dn );
994        $s = $self->search({
995            base   => $dn,
996            vals   => 1,
997            attrs  => \@attrs
998        });
999    }
1000
1001    if ( $s->{'code'} ) {
1002        print $s->{'message'} . "\n";
1003        return;
1004    }
1005
1006    foreach my $e ( @{ $s->{'entries'} } ) {
1007        $self->ldif->write_entry( $e );
1008        print "\n";
1009    }
1010    return;
1011}
1012
1013sub run_cd 
1014{
1015    my $self    = shift;
1016    my $newbase = join ' ', @_;
1017
1018    # convert given path to a DN
1019    $newbase = $self->path_to_dn( $newbase );
1020
1021    unless ( $self->is_valid_dn( $newbase ) ) {
1022        print "No such object\n";
1023        return;
1024    }
1025
1026    # store old base
1027    $self->{'previous_base'} = $self->base();
1028
1029    # update new base
1030    $self->base( $newbase );
1031
1032    # get new 'cwd' listing
1033    my $s = $self->search({ scope => 'one', attrs => [ '1.1' ] });
1034    if ( $s->{'code'} ) {
1035        print "$s->{'message'}\n";
1036        return;
1037    }
1038    $self->update_entries( search => $s );
1039
1040    # reflect cwd change in prompt
1041    $self->update_prompt();
1042    return;
1043}
1044
1045sub run_clear
1046{
1047    my $self = shift;
1048    system('clear');
1049    return;
1050}
1051
1052sub run_copy
1053{
1054    my $self = shift;
1055    my ( $s_dn, $d_dn ) = @_;
1056
1057    unless ( $s_dn ) {
1058        print "No source dn provided.\n";
1059        return;
1060    }
1061    unless ( $d_dn ) {
1062        print "No destination dn provided.\n";
1063        return;
1064    }
1065
1066    # convert given source path to DN
1067    $s_dn = $self->path_to_dn( $s_dn );
1068
1069    my $s = $self->search({ base => $s_dn, vals => 1 });
1070    unless ( $s->{'code'} == LDAP_SUCCESS ) {
1071        print "No such object\n";
1072        return;
1073    }
1074
1075    # see if we're copying the entry to a totally new path
1076    my ( $new_dn, $old_dn );
1077    ( $d_dn, $new_dn ) = ( $1, $2 ) if $d_dn =~ /^([\w=]+),(.*)$/;
1078    if ( $new_dn ) {
1079        unless ( $self->is_valid_dn( $new_dn ) ) {
1080            print "Invalid destination.\n";
1081            return;
1082        }
1083    }
1084    else {
1085        $new_dn = $self->base();
1086    }
1087    $old_dn = $1 if $s_dn =~ /^[\w=]+,(.*)$/;
1088
1089    # get the source object
1090    my $e = ${ $s->{'entries'} }[0];
1091    $e->dn( $s_dn );
1092
1093    # add changes in new entry instead of modifying existing
1094    $e->changetype('add'); 
1095    $e->dn( "$d_dn,$new_dn" );
1096
1097    # get the unique attribute from the dn for modification
1098    # perhaps there is a better way to do this...?
1099    #
1100    my ( $uniqkey, $uniqval ) = ( $1, $2 )
1101      if $d_dn =~ /^([\.\w\-]+)(?:\s+)?=(?:\s+)?([\.\-\s\w]+),?/;
1102    unless ( $uniqkey && $uniqval ) {
1103        print "Unable to parse unique values from rdn.\n";
1104        return;
1105    }
1106    $e->replace( $uniqkey => $uniqval );
1107
1108    # update
1109    my $rv = $e->update( $self->ldap() );
1110    print $rv->error , "\n";
1111
1112    # clear caches
1113    $self->{'cache'}->{ $new_dn } = {} if $new_dn;
1114    $self->{'cache'}->{ $old_dn } = {} if $old_dn;
1115    $self->update_entries( clearcache => 1 );
1116    return;
1117}
1118
1119sub run_create
1120{
1121    my $self = shift;
1122    my @ocs  = @_;
1123
1124    my ( $fh, $fname ) =
1125        File::Temp::tempfile( 'shelldap_XXXXXXXX', DIR => '/tmp', UNLINK => 1 );
1126
1127    # first print out the dn and object classes.
1128    print $fh 'dn: ???,', $self->base(), "\n";
1129    foreach my $oc ( sort @ocs ) {
1130        print $fh "objectClass: $oc\n";
1131    }
1132
1133    # now gather attributes for requested objectClasses
1134    #
1135    my ( %seen, @must_attr, @may_attr );
1136    foreach my $oc ( sort @ocs ) {
1137
1138        # required
1139        my @must = $self->{'schema'}->must( $oc );
1140        foreach my $attr ( sort { $a->{'name'} cmp $b->{'name'} } @must ) {
1141            next if $attr->{'name'} =~ /^objectclass$/i;
1142            next if $seen{ $attr->{'name'} };
1143            push @must_attr, $attr->{'name'};
1144            $seen{ $attr->{'name'} }++;
1145        }
1146
1147        # optional
1148        my @may  = $self->{'schema'}->may( $oc );
1149        foreach my $attr ( sort { $a->{'name'} cmp $b->{'name'} } @may ) {
1150            next if $attr->{'name'} =~ /^objectclass$/i;
1151            next if $seen{ $attr->{'name'} };
1152            push @may_attr, $attr->{'name'};
1153            $seen{ $attr->{'name'} }++;
1154        }
1155    }
1156
1157    # print attributes
1158    print $fh "$_: \n"   foreach @must_attr;
1159    print $fh "# $_: \n" foreach @may_attr;
1160    close $fh;
1161    my $hash_a = $self->chksum( $fname );
1162    system( $self->{'editor'}, $fname ) && die "Unable to launch editor: $!\n";
1163
1164    # hash compare
1165    my $hash_b = $self->chksum( $fname );
1166    if ( $hash_a eq $hash_b ) {
1167        print "Entry not modified.\n";
1168        unlink $fname;
1169        return;
1170    }
1171
1172    # load in LDIF
1173    my $ldif = Net::LDAP::LDIF->new( $fname, 'r', onerror => 'warn' );
1174    my $e   = $ldif->read_entry();
1175    unless ( $e ) {
1176        print "Unable to parse LDIF.\n";
1177        unlink $fname;
1178        return;
1179    }
1180    $e->changetype('add');
1181    my $rv = $e->update( $self->ldap() );
1182    print $rv->error(), "\n";
1183
1184    $self->update_entries( clearcache => 1 ) unless $rv->code();
1185
1186    unlink $fname;
1187    return;
1188}
1189
1190sub run_delete
1191{
1192    my $self = shift;
1193    my @DNs  = @_;
1194
1195    unless ( scalar @DNs ) {
1196        print "No dn specified.\n";
1197        return;
1198    }
1199
1200    my $filter;
1201    unless ( $DNs[0] eq '*' ) {
1202        $filter = $self->make_filter( \@DNs ) or return;
1203    }
1204
1205    my $s = $self->search({ scope => 'one', filter => $filter });
1206    if ( $s->{'code'} ) {
1207        print "$s->{'message'}\n";
1208        return;
1209    }
1210
1211    print "Are you sure? [N/y]: ";
1212    chomp( my $resp = <STDIN> );
1213    return unless $resp =~ /^y/i;
1214   
1215    foreach my $e ( @{ $s->{'entries'} } ) {
1216        my $dn = $e->dn();
1217        my $rv = $self->ldap->delete( $dn );
1218        print "$dn: ", $rv->error(), "\n";
1219    }
1220   
1221    $self->update_entries( clearcache => 1 );
1222    return;
1223}
1224
1225sub run_edit
1226{
1227    my $self = shift;
1228    my $dn   = join ' ', @_;
1229
1230    unless ( $dn ) {
1231        print "No dn provided.\n";
1232        return;
1233    }
1234
1235    # convert given path to DN
1236    $dn = $self->path_to_dn( $dn );
1237
1238    my $s = $self->search({ base => $dn, vals => 1 });
1239
1240    if ( $s->{'code'} ) {
1241        print $s->{'message'} . "\n";
1242        return;
1243    }
1244
1245    # fetch entry and write it out to disk
1246    my $e = ${ $s->{'entries'} }[0];
1247    my $ldif = $self->ldif(1);
1248    $ldif->write_entry( $e );
1249    $ldif->done();  # force sync
1250
1251    # load it into an array for potential comparison
1252    open LDIF, "$self->{'ldif_fname'}" or return;
1253    my @orig_ldif = <LDIF>;
1254    close LDIF;
1255
1256    # checksum it, then open it in an editor
1257    my $hash_a = $self->chksum( $self->{'ldif_fname'} );
1258    system( "$self->{'editor'} $self->{'ldif_fname'}" ) &&
1259        die "Unable to launch editor: $!\n";
1260
1261    # detect a total lack of change
1262    my $hash_b = $self->chksum( $self->{'ldif_fname'} );
1263    if ( $hash_a eq $hash_b ) {
1264        print "Entry not modified.\n";
1265        unlink $self->{'ldif_fname'};
1266        return;
1267    }
1268
1269    # check changes for basic LDIF validity
1270    my $new_e = $self->load_ldif( $self->{'ldif_fname'} );
1271    unless ( $new_e ) {
1272        print "Unable to parse LDIF.\n";
1273        unlink $self->{'ldif_fname'};
1274        return;
1275    }
1276
1277    # load changes into a new array for comparison
1278    open LDIF, "$self->{'ldif_fname'}" or return;
1279    my @new_ldif = <LDIF>;
1280    close LDIF;
1281
1282    $e->changetype('modify');
1283
1284    my $parse = sub {
1285        my $line = shift || $_;
1286        return if $line  =~ /^\#/; # ignore comments
1287        my ( $attr, $val ) = ( $1, $2 ) if $line =~ /^(.+?): (.*)$/;
1288        return unless $attr;
1289        return if index($attr, ':') != -1;  # ignore base64
1290        return ( $attr, $val );
1291    };
1292
1293    unwrap( \@orig_ldif );
1294    unwrap( \@new_ldif );
1295
1296    my $diff = Algorithm::Diff->new( \@orig_ldif, \@new_ldif );
1297    HUNK:
1298    while ( $diff->Next() ) {
1299        next if $diff->Same();
1300        my $diff_bit = $diff->Diff();
1301        my %seen_attr;
1302
1303        # total deletions
1304        if ( $diff_bit == 1 ) {
1305            foreach ( $diff->Items(1) ) {
1306                $self->debug("DELETE: $_");
1307                my ( $attr, $val ) = $parse->( $_ ) or next;
1308                $e->delete( $attr => [ $val ] );
1309            }
1310        }
1311
1312        # new insertions
1313        if ( $diff_bit == 2 ) {
1314            foreach ( $diff->Items(2) ) {
1315                $self->debug("INSERT: $_");
1316                my ( $attr, $val ) = $parse->( $_ ) or next;
1317                $e->add( $attr => $val );
1318            }
1319        }
1320
1321        # replacements
1322        if ( $diff_bit == 3 ) {
1323            foreach ( $diff->Items(2) ) {
1324                $self->debug("MODIFY: $_");
1325                my ( $attr, $val ) = $parse->( $_ ) or next;
1326
1327                my $cur_vals = $e->get_value( $attr, asref => 1 ) || [];
1328                my $cur_valcount = scalar @$cur_vals;
1329                next if $cur_valcount == 0; # should have been an 'add'
1330
1331                # replace immediately
1332                #
1333                if ( $cur_valcount == 1 ) {
1334                    $e->replace( $attr => $val );
1335                }
1336                else {
1337
1338                    # make sure the replace doesn't squash
1339                    # other attributes listed with the same name
1340                    #
1341                    next if $seen_attr{ $attr };
1342                    my @new_vals;
1343                    foreach my $line ( @new_ldif ) {
1344                        my ( $new_attr, $new_val ) = $parse->( $line ) or next;
1345                        next unless $new_attr eq $attr;
1346                        $seen_attr{ $attr }++;
1347                        push @new_vals, $new_val;
1348                    }
1349                    $e->replace( $attr => \@new_vals );
1350                }
1351            }
1352        }
1353
1354    }
1355
1356    unlink $self->{'ldif_fname'};
1357    my $rv = $e->update( $self->ldap );
1358    print $rv->error(), "\n";
1359
1360    return;
1361}
1362
1363sub run_env
1364{
1365    my $self = shift;
1366
1367    foreach ( sort @{ $self->{'env'} } ) {
1368        print "$_: ";
1369        print $conf->{$_} ? $conf->{$_} : 0; 
1370        print "\n"
1371    }
1372}
1373
1374sub run_grep
1375{
1376    my $self = shift;
1377    my ( $recurse, $filter, $base ) = @_;
1378
1379    # set 'recursion'
1380    unless ( $recurse && $recurse =~ /\-r|recurse/ ) {
1381        # shift args to the left
1382        ( $recurse, $filter, $base ) = ( undef, $recurse, $filter );
1383    }
1384
1385    $filter = Net::LDAP::Filter->new( $filter );
1386    unless ( $filter ) {
1387        print "Invalid search filter.\n";
1388        return;
1389    }
1390
1391    # support '*'
1392    $base = $self->base() if ! $base or $base eq '*';
1393
1394    unless ( $base ) {
1395        print "No search base specified.\n";
1396        return;
1397    }
1398
1399    # convert base path to DN
1400    $base = $self->path_to_dn( $base );
1401
1402    $self->debug("Filter parsed as: " . $filter->as_string() . "\n");
1403
1404    my $s = $self->search(
1405        {
1406            scope  => $recurse ? 'sub' : 'one',
1407            base   => $base,
1408            filter => $filter
1409        }
1410    );
1411
1412    foreach my $e ( @{ $s->{'entries'} } ) {
1413        my $dn = $e->dn();
1414        print "$dn\n";
1415    }
1416
1417    return;
1418}
1419
1420# override internal help functions
1421# with pod2usage
1422#
1423sub run_help 
1424{
1425    return Pod::Usage::pod2usage(
1426        -exitval  => 'NOEXIT',
1427        -verbose  => 99,
1428        -sections => 'SHELL COMMANDS'
1429    );
1430}
1431
1432sub run_list
1433{
1434    my $self  = shift;
1435    my @args  = @_;
1436    my @attrs = ();
1437    my $filter;
1438
1439    # flag booleans
1440    my ( $recurse, $long );
1441
1442    # parse arguments: [ <option> ...] [<filter> ...] [<attribute> ...]
1443    if ( scalar @args ) {
1444        # options: support '-l' or '-R' listings
1445        if ( $args[0] =~ /^\-(\w+)/o ) {
1446            my $flags = $1;
1447            $recurse  = $flags =~ /R/;
1448            $long     = $flags =~ /l/;
1449            shift( @args );
1450        }
1451
1452        my @filters;
1453
1454        # get filter elements from argument list
1455        while ( @args && $self->is_valid_filter($args[0]) ) {
1456            push( @filters, shift(@args) );
1457        }
1458
1459        push( @filters, '(objectClass=*)' ) unless scalar @filters;
1460       
1461        # construct OR'ed filter from filter elements
1462        $filter = $self->make_filter( \@filters );
1463
1464        # remaining arguments must be attributes
1465        push( @attrs, @args );
1466    }
1467
1468    # Get all attributes if none are specified, and we're in long-list mode.
1469    push( @attrs, '*' )  if $long && ! scalar @attrs;
1470
1471    my $s = $self->search({
1472        scope  => $recurse ? 'sub' : 'one',
1473        vals   => 1,
1474        filter => $filter,
1475        attrs  => [ @attrs, 'hasSubordinates' ]
1476    });
1477    if ( $s->{'code'} ) {
1478        print "$s->{'message'}\n";
1479        return;
1480    }
1481
1482    # if an entry doesn't have a description field,
1483    # try and show some nice defaults for ls -l !
1484    #
1485    # objectClass -> Attribute to show
1486    #
1487    my %descs = %{
1488        $conf->{'descmaps'}
1489          || {
1490            posixAccount => 'gecos',
1491            posixGroup   => 'gidNumber',
1492            ipHost       => 'ipHostNumber',
1493          }
1494      };
1495
1496    # iterate and print
1497    #
1498    my $dn_count = 0;
1499    my $base = $self->base();
1500    foreach my $e ( sort { $a->dn() cmp $b->dn() } @{ $s->{'entries'} } ) {
1501        my $dn = $e->dn();
1502        next if lc( $dn ) eq lc( $base );
1503
1504        if ( ! $long ) {
1505            # strip the current base from the dn, if we're recursing and not in long mode
1506            if ( $recurse ) {
1507                $dn =~ s/,$base//oi;
1508            }
1509
1510            # only show RDN unless -l was given
1511            else {
1512                $dn = canonical_dn( [shift(@{ldap_explode_dn($dn, casefold => 'none')})], casefold => 'none' )
1513            }
1514        }
1515
1516        # if this entry is a container for other entries, append a
1517        # trailing slash.
1518        $dn .= '/'  if $e->get_value('hasSubordinates') &&
1519            $e->get_value('hasSubordinates') eq 'TRUE';
1520
1521        # additional arguments/attributes were given; show their values
1522        #
1523        if ( scalar @args ) {
1524            my @elements = ( $dn );
1525
1526            foreach my $attr ( @args ) {
1527                my @vals = $e->get_value( $attr );
1528                push( @elements, join(',', @vals) );
1529            }
1530
1531            print join( "\t", @elements )."\n";
1532        }
1533        else {
1534            # show descriptions
1535            my $desc = $e->get_value( 'description' );
1536            if ( $desc ) {
1537                $desc =~ s/\n.*//s; # 1st line only
1538                $dn .= " ($desc)";
1539            }
1540
1541            # no desc?  Try and infer something useful
1542            # to display.
1543            else {
1544
1545                # pull objectClasses, hash for lookup speed
1546                my @oc   = $e->get_value( 'objectClass' );
1547                my %ochash;
1548                map { $ochash{$_} = 1 } @oc;
1549
1550                foreach my $d_listing ( sort keys %descs ) {
1551                    if ( exists $ochash{ $d_listing } ) {
1552                        my $str = $e->get_value( $descs{ $d_listing }, asref => 1 );
1553                        $dn .= ' (' . (join ', ', @$str) . ')' if $str && scalar @$str;
1554                    }
1555                    next;
1556                }
1557            }
1558            print "$dn\n";
1559        }
1560        $dn_count++;
1561    }
1562   
1563    print "\n$dn_count " .
1564        ( $dn_count == 1 ? 'object.' : 'objects.') .
1565        "\n" if $long;
1566    return;
1567}
1568
1569sub run_mkdir
1570{
1571    my $self = shift;
1572    my $dir  = join ' ', @_;
1573
1574    unless ( $dir ) {
1575        print "No 'directory' provided.\n";
1576        return;
1577    }
1578
1579    # normalize name, if it is not yet a legal DN
1580    $dir = 'ou=' . $dir unless canonical_dn( $dir );
1581
1582    # convert given path to full DN
1583    $dir = $self->path_to_dn( $dir );
1584
1585    # get RDN: naming attributes (lower-case) and their values
1586    my %rdn = %{ shift(@{ ldap_explode_dn($dir, casefold => 'lower') }) };
1587
1588    # add
1589    my $r = $self->ldap()->add( $dir, attr => [
1590        objectClass => [ 'top', 'organizationalUnit' ], %rdn
1591    ]);
1592
1593    print $r->error(), "\n";
1594    $self->update_entries( clearcache => 1 );
1595    return;
1596}
1597
1598sub run_move
1599{
1600    my $self = shift;
1601    my ( $s_dn, $d_dn ) = @_;
1602
1603    unless ( $s_dn ) {
1604        print "No source dn provided.\n";
1605        return;
1606    }
1607    unless ( $d_dn ) {
1608        print "No destination dn provided.\n";
1609        return;
1610    }
1611
1612    # convert given source path to DN
1613    $s_dn = $self->path_to_dn( $s_dn );
1614
1615    unless ( $self->is_valid_dn( $s_dn ) ) {
1616        print "No such object\n";
1617        return;
1618    }
1619
1620    # see if we're moving the entry to a totally new path
1621    my ( $new_dn, $old_dn );
1622    ( $d_dn, $new_dn ) = ( $1, $2 ) if $d_dn =~ /^([\w=]+),(.*)$/;
1623    $old_dn = $1 if $s_dn =~ /^[\w=]+,(.*)$/;
1624
1625    my $rv = $self->ldap()->moddn(
1626        $s_dn,
1627        newrdn       => $d_dn,
1628        deleteoldrdn => 1,
1629        newsuperior  => $new_dn
1630    );
1631    print $rv->error(), "\n";
1632
1633    # clear caches
1634    $self->{'cache'}->{ $new_dn } = {} if $new_dn;
1635    $self->{'cache'}->{ $old_dn } = {} if $old_dn;
1636    $self->update_entries( clearcache => 1 );
1637    return;
1638}
1639
1640sub run_passwd 
1641{
1642    my $self = shift;
1643    my $dn   = shift || $self->base();
1644
1645    $self->{'root_dse'} ||= $self->ldap->root_dse();
1646
1647    my $pw_extension = '1.3.6.1.4.1.4203.1.11.1';
1648    unless ( $self->{'root_dse'}->supported_extension( $pw_extension ) ) {
1649        print "Sorry, password changes not supported by LDAP server.\n";
1650        return;
1651    }
1652
1653    # convert given path to DN
1654    $dn = $self->path_to_dn( $dn );
1655
1656    my $s = $self->search( { base => $dn, scope => 'base' } );
1657    if ( $s->{'code'} ) {
1658        print $s->{'message'}, "\n";
1659        return;
1660    }
1661    my $e = ${ $s->{'entries'} }[0];
1662
1663    unless ( $e->exists('userPassword') ) {
1664        print "No userPassword attribute for $dn\n";
1665        return;
1666    }
1667
1668    print "Changing password for $dn\n";
1669    Term::ReadKey::ReadMode 2;
1670    print "Enter new password: ";
1671    chomp( my $pw  = <STDIN> );
1672    print "\nRetype new password: ";
1673    chomp( my $pw2 = <STDIN> );
1674    print "\n";
1675    Term::ReadKey::ReadMode 0;
1676
1677    if ( $pw ne $pw2 ) {
1678        print "Sorry, passwords do not match.\n";
1679        return;
1680    }
1681
1682    my $rv = $self->ldap->set_password(
1683        user      => $dn,
1684        newpasswd => $pw
1685    );
1686
1687    if ( $rv->code() == LDAP_SUCCESS ) {
1688        print "Password updated successfully.\n";
1689    } else {
1690        print "Password error: " . $rv->error() . "\n";
1691    }
1692
1693    return;
1694}
1695
1696sub run_pwd 
1697{
1698    my $self = shift;
1699    print $self->base() . "\n";
1700    return;   
1701}
1702
1703sub run_setenv
1704{
1705    my $self = shift;
1706    my ( $key, $val ) = @_;
1707
1708    ( $key, $val ) = split /=/, $key if $key && ! defined $val;
1709    return unless $key && defined $val;
1710    $key = lc $key;
1711
1712    $conf->{$key} = $val;
1713    return;
1714}
1715
1716sub run_whoami
1717{
1718    my $self = shift;
1719    print $conf->{'binddn'} || 'anonymous bind';
1720    print "\n";
1721    return;
1722}
1723
1724###############################################################
1725#
1726# MAIN
1727#
1728###############################################################
1729
1730package main;
1731use strict;
1732use warnings;
1733
1734$0 = 'shelldap';
1735my $VERSION = '0.5';
1736
1737use Getopt::Long;
1738use YAML::Syck;
1739use Pod::Usage;
1740eval 'use Term::ReadLine::Gnu';
1741warn qq{Term::ReadLine::Gnu not installed.
1742Continuing, but shelldap is of limited usefulness without it.\n\n} if $@;
1743
1744# get config - rc file first, command line overrides
1745use vars '$conf';
1746$conf = load_config() || {};
1747Getopt::Long::GetOptions(
1748    $conf, 
1749    'server|H=s',
1750    'binddn|D=s',
1751    'basedn|b=s',
1752    'cacheage=i',
1753    'timeout=i',
1754    'tls_cacert=s',
1755    'tls_cert=s',
1756    'tls_key=s',
1757    'tls', 'debug', 'version',
1758     help => sub {
1759        Pod::Usage::pod2usage(
1760            -verbose => 1,
1761            -message => "\n$0 command line flags\n" . '-' x 65
1762        );
1763    }
1764);
1765
1766# show version
1767if ( $conf->{'version'} ) {
1768    print "$VERSION\n";
1769    exit( 0 );
1770}
1771
1772# defaults
1773$conf->{'confpath'} = "$ENV{'HOME'}/.shelldap.rc";
1774$conf->{'cacheage'} ||= 300;
1775$conf->{'timeout'}  ||= 10;
1776
1777# create and enter shell loop
1778my $shell = LDAP::Shell->new();
1779$shell->cmdloop();
1780
1781# load YAML config into global conf.
1782#
1783sub load_config
1784{
1785    my ( $d, $data );
1786
1787    my $confpath;
1788    my @confs = (
1789        "$ENV{'HOME'}/.shelldap.rc",
1790        '/usr/local/etc/shelldap.conf',
1791        '/etc/shelldap.conf',
1792    );
1793    foreach ( @confs ) {
1794        if ( -e $_ ) {
1795            $confpath = $_;
1796            last;
1797        }
1798    }
1799    $confpath or return undef;
1800
1801    open YAML, $confpath or return undef;
1802    do {
1803        local $/ = undef;
1804        $data = <YAML>;  # slurp!
1805    };
1806    close YAML;
1807
1808    eval { $conf = YAML::Syck::Load( $data ) };
1809    die "Invalid YAML in $confpath\n" if $@;
1810
1811    return $conf;
1812}
1813
1814## EOF
1815
Note: See TracBrowser for help on using the repository browser.