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
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
- 2006-04-27 Version 1.0
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__