LDAP wrapper for ypcat, ypwhich, ypmatch commands.

I’m converting a site over to LDAP from NIS. It’s a legacy HPC shop with dollars on the line for every minute of unavailability, so there’s lots of stuff that “they”, both vendors and customers, don’t really want to fix if it ain’t broke. Converting from NIS to LDAP is one of those sticky gray areas, especially when you’re talking about decades of legacy. Fortunately the problem area is restricted to just administrative stuff — no end-user type of access was ever supported. Limits the machines running special things to just a handful.

Having taken a good look at the various NIS/LDAP wrappers available, none really seemed to fit the bill precisely in a clean enough fashion for my sense of taste… My airplane reading for last year’s SAGE conference was Perl Best Practices, and I’ve noticed a marked improvement in the quality of code that I’ve been able to write since then. Following some of the guidelines, such as leaning on Pod::Usage for automatic documentation, and designing around the data, really makes an impact. Writing code that catches error conditions and reports on them in meaningful ways is also a useful practice — I’m particularly fond of the beginning section that pulls in all the required perl modules in a graceful(tm) fashion.


Output from perltidy -html ypspoof


ypspoof

Code Index:



NAME

ypspoof – A wrapper for ypcat and ypmatch that uses LDAP.


SYNOPSIS

ypcat-gfdl [options] mapname

ypcat [options] mapname

ypmatch [options] key mapname

ypmatch [-x] [-m]

 Options:
   --help            brief help message
   --man             full documentation
   --debug           debugging mode
   -m                ypwhich compatibility flag; only known maps output.
   -x                ypwhich compatibility flag; shows NIS nicknames.


OPTIONS

-help
Print a brief help message and exits.

-man
Prints the manual page and exits.


DESCRIPTION

ypcat emulates the functionality of the YP ‘ypcat’ command.

ypmatch emulates the functionality of the YP ‘ypmatch’ command.

ypwhich emulates the functionality of the YP ‘ypwhich’ command.

This code is perl module heavy to be as clean as possible.

It should be able to handle all LDAP connections via Net::LDAP,
although mismatched/missing TLS certificates for ldaps (port 636) have
not been fully debugged.

It uses OpenLDAP’s /etc/openldap/ldap.conf configuration file.

It logs usage via syslog local3.info facility.


BUGS

Not implemented:

 NIS Mapname            Reason
 auto.master              multiple ways to resolve query
 bootparams               Usually empty
 ethers.byname,byaddr     Usually empty
 hosts.byaddr,byname      DNS. 'nuf said.
 mail.aliases             hm.. mailAlternateAddress, mail, mailbox..
 netgroup                 would need to see if NSD deal with this somehow..
 netid.byname             Usually empty
 netmasks.byaddr          Usually empty
 networks.byaddr,byname   Usually empty
 protocols.bynumber       hm..  in the schema...
 publickey.byname         Uuuh.. RH specific?
 rpc.bynumber,byname      hm. in the schema..
 services.byname          hm. in the schema..
 ypservers                oh puleeze.

Other not implemented:

 .ldaprc profile reading ala OpenLDAP.
 /etc/ldap.conf


HISTORY

  1. 2006-04-27 Version 1.0
  2. Baseline functionality, for passwd and group. Should expand reasonably
    well to other maps, except for .. automount… and maybe netgroup.


AUTHOR

Chan Wilson, ypspoof@confusedhacker.net
2006-04-26


COPYRIGHT

    Copyright (C) 2006 Chandin Wilson, ypspoof@confusedhacker.net
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

#!/usr/bin/perl
# Irix chp_perl lives in /opt/perl/bin.
$] < 5.008 && $^O =~ /irix/ && exec '/opt/perl/bin/perl', $0, @ARGV;

die "Missing required Perl modules: ", join ' ', @modules, "n"
  if map {eval "use $_"; push @modules, $_ if $@; @modules } qw{
     strict English
     File::Basename
     Pod::Usage
     Getopt::Long
     Net::LDAP::Express
     Config::General }
  , 'Sys::Syslog qw(:DEFAULT setlogsock)';

my $mode = basename($0);
my $LDAP_CONFIG_FILE = '/etc/openldap/ldap.conf';

my %yp_maps = 
  (
   passwd => {
              attributes => [
                             'uid', 'userPassword',  'uidNumber', 'gidNumber',
                             'cn',  'homeDirectory', 'loginShell'
                             ],
              filter      => '(objectClass=posixAccount)',
              sortby      => 'uid',
              match_attr => 'uid',
              nickname    => 'passwd.byname',
             },
   'passwd.byuid' => {
              attributes => [
                             'uid', 'userPassword',  'uidNumber', 'gidNumber',
                             'cn',  'homeDirectory', 'loginShell'
                             ],
              filter      => '(objectClass=posixAccount)',
              sortby      => 'uidNumber',
              match_attr => 'uidNumber',
             },
   group  => {
              attributes => [ 
                             'cn', 'userPassword', 'gidNumber', 'memberUid',
                             ],
              filter      => '(objectClass=posixGroup)',
              sortby      => 'cn',
              match_attr => 'cn',
              nickname    => 'group.byname',
             },
   'group.bygid'  => {
              attributes => [ 
                             'cn', 'userPassword', 'gidNumber', 'memberUid',
                             ],
              filter      => '(objectClass=posixGroup)',
              sortby      => 'gidNumber',
              match_attr => 'gidNumber',
             },

);


## Parse options and print usage if there is a syntax error,
## or if usage was explicitly requested.
my ($man, $help, $DEBUG, $master, $nicks) = 0;
GetOptions('help|?'  => $help,  man => $man,
           'debug|d' => $DEBUG, m => $master,
           x         => $nicks) or pod2usage(2);
my ($KEY, $MAP);

if    ($mode =~ /cat/)   { $MAP = $ARGV[0]; }
elsif ($mode =~ /match/) { ($KEY, $MAP) = @ARGV[0,1]; }
elsif ($mode !~ /which/) {
  pod2usage(-verbose => 2,
            -message => "Unknown linkage. Potential links:n")
}

pod2usage(1) if $help;
pod2usage(-verbose => 2) if $man;
pod2usage(-verbose => 1,
          -message => "Unknown map name. Valid maps are:nt" .
          join("t", keys %yp_maps),
          )
  if $mode !~ /which/ && ! grep( $yp_maps{$_}, $MAP );

# Parse the config file and make sure all needed entries
# are present and transformed as appropriate, so the config
# hash can simply be passed along to Net::LDAP::Express

my %ldap_config = ParseConfig($LDAP_CONFIG_FILE);
die "Need a connection method defined in $LDAP_CONFIG_FILE" 
  unless $ldap_config{URI} || $ldap_config{HOST};
die "Need a base dn to defined in $LDAP_CONFIG_FILE"
  unless $ldap_config{BASE};

# NLE expects lowercase opts..
%ldap_config = ( %ldap_config, 
                 map { lc $_ => $ldap_config{$_} } keys %ldap_config );
# ... and host/port vs URI ...
if ($ldap_config{URI}) {
  ($ldap_config{host}) = $ldap_config{URI} =~ /^(S+)/;
}

# Requisites satisfied, let's do something.

openlog($mode, 'pid', 'local3');
setlogsock('stream') if $^O eq 'irix';

#---------
# Handle ypwhich functionality up top, since it doesn't require
# actually talking to the LDAP server.

if ($mode =~ /which/) {
  syslog('info', "($EUID/$EGID) ypwhich " . ($master ? '-m' : $nicks ? '-x' : '') );

  # these all exit.
  print map { "$_ nobody-but-me-myself-and-in"} keys %yp_maps
    if $master;
  print map { qq{Use "$_" for map "$yp_maps{$_}->{nickname}"n} 
                if $yp_maps{$_}->{nickname}} keys %yp_maps
    if $nicks;
  print "nobody-but-me-myself-and-in" if !$master && !$nicks;
  exit;
}

#---------
# connect_to_ldap() should hide all sorts of gory details, like TLS
# problems, timeouts, etc.  Currently it relies on Net::LDAP::Express
# (and hence Net::LDAP and Net::LDAPS) for all these details which
# should be the proper approach.

my $ldap = 
  connect_to_ldap({config   => %ldap_config,
                   attributes    => $yp_maps{$MAP}{attributes}
                  });
die "Can't connect to source ldap server: $@" if ($@);

#---------
# ypcat functionality block

if ($mode =~ /cat/) {
  syslog('info', "($EUID/$EGID) $MAP");

  my $entries = $ldap->search( filter => $yp_maps{$MAP}{filter} );
  die "Uh, no entries found in ldap database?!?" 
    unless $entries->count > 1;

  for my $ldap_entry ($entries->entries) {
    DEBUG("loop: dn:" . $ldap_entry->dn);
    print
      join(':',
           map { $ldap_entry->get_value($_) || 'x' }
           @{$yp_maps{$MAP}{attributes}}), "n";
  }
  exit 0;
}

#----------
# ypmatch functionaliyt block.

else {
  syslog('info', "($EUID/$EGID) $KEY $MAP");
  my $entry = $ldap->search( filter => '(&' . $yp_maps{$MAP}{filter} 
                             . "($yp_maps{$MAP}{match_attr}=$KEY))");
  if ($entry->count < 1) { 
    die "Can't match key $KEY in map $MAP. Reason: No such key in mapn";
  }
  for my $ldap_entry ($entry->entries) {
    DEBUG("loop: dn:" . $ldap_entry->dn);
    print 
      join(':',
           map { $ldap_entry->get_value($_) || 'x' }
           @{$yp_maps{$MAP}{attributes}}), "n";
  }
  exit 0;
}

# infamous 'never reach here' line.
exit 1;
#----------------------------------------
# utility stub to wrap the ldap connectivity madness.
# returns an object that has a search method, ala
# Net::LDAP::Express or Net::LDAP.

sub connect_to_ldap {
  my $config       = $_[0]->{config};
  my $attributes = $_[0]->{attributes};
  my $ldap;

  return unless
    $ldap =
      Net::LDAP::Express->new(%{$config},
                            searchattrs => $attributes
                              );
  return $ldap;
}

#----------------------------------------
# generic debug print routine.
sub DEBUG { 
  print STDERR "DEBUG: $_[0]n"
    if $DEBUG;
}



__END__