# configuration for ldap
package Net::LDAP::Config;
use strict;
use Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK $VERSION $AUTOLOAD $CONFIG);
=head1 NAME
Net::LDAP::Config - a simple wrapper for maintaining info related to LDAP
connections
=head1 SYNOPSIS
my $config = Net::LDAP::Config->new('source' => 'default');
$config->clauth(); # CLI authentation
$config->bind(
'dn' => $dn,
'password' => $password
); # normal authentation
=head1 DESCRIPTION
B<Net::LDAP::Config> is a wrapper module originally written
for B<ldapsh> but which is useful for much more. It's not very well
documented just yet, but here are the main uses:
=head1 CONFIG FILE
The config file is a simple INI-style format. There is one special section,
B<main>, and the only option it recognizes is B<default>, for specifying
the default source. Any other sections specify an LDAP source.
For example:
[ldap]
servers: ldap1.domain.com,ldap2.domain.com
base: dc=domain,dc=com
ssl: require
[main]
default: ldap
A main config file is looked for in /etc/ldapsh_config and
/usr/local/etc/ldapsh_config, and then in the user's home directory, either
in the file specified by $LDAP_CONFIG or ~/.ldapsh_config.
=head1 CLI AUTHENTICATION
If you are building an interactive script, you'll want to use this method:
create the configuration object, which basically pulls the server
configuration from the config file
my $config = Net::LDAP::Config->new('source' => 'mysource');
and then get all of the necessary info
this caches ldap UIDs in ~/.ldapuids
$config->clauth();
=head1 NORMAL AUTHENTICATION
This is where you collect the DN and password and auth normally:
my $config = Net::LDAP::Config->new('source' => 'mysource');
$config->bind(
'dn' => $dn,
'password' => $password
); # normal authentation
If you don't want to authenticate, use B<connect>:
my $config = Net::LDAP::Config->new('source' => 'mysource');
$config->connect();
Yes, it sucks that there's a difference. I'm still trying
to clean up the API.
You should probably just use B<bind>, as it behaves well
either with or without auth information.
=head1 ENVIRONMENT VARIABLES
Here are the environment variables that B<Net::LDAP::Config> uses:
=over 4
=item LDAP_UIDFILE
The file in which to store LDAP DN's. Defaults to ~/.ldapuids.
This file is maintained automatically by B<Net::LDAP::Config>, although
you can modify it if you like -- it just caches the searched-for DN
so you don't have to specify your username each time.
Feel free to recommend a different design.
=item LDAP_CONFIG
A user-specific config file; over-rides any information in the central
file. Defaults to ~/.ldapsh_config.
=back
=head1 FUNCTIONS
=over 4
=cut
#---------------------------------------------------------------
#---------------------------------------------------------------
# Code that everyone will use
#-----------------------------------------------------------------
# debug
=item debug
Can be used to turn debugging on (debug("on")) or off (debug("off")),
otherwise prints on STDERR anything passed to it if debugging is
currently on.
=cut
sub debug {
if ($_[0]) {
$_[0] =~ /^on$/i and do {
warn "turning debug on\n";
$Net::LDAP::DEBUG = 1;
return;
};
$_[0] =~ /^off$/i and do {
warn "turning debug off\n";
$Net::LDAP::DEBUG = 0;
return;
};
} else {
return $Net::LDAP::DEBUG || 0;
}
unless ($Net::LDAP::DEBUG) { return; }
if (@_) {
warn "$0: @_\n";
}
return 1;
}
# debug
#-----------------------------------------------------------------
#-----------------------------------------------------------------
# error
=item error
Used to store and report errors on the shell. Any arguments
passed to B<error> are joined into a single error message and
returned as an error any time B<error> is called.
EXAMPLE
=over 4
if ( error() ) { warn error("There was a problem"); }
else { dostuff(); }
if (error()) { die error(); }
=back
=cut
sub error {
if (@_) {
$Net::LDAP::ERROR = join(' ', @_) . "\n";
}
if ($Net::LDAP::ERROR) {
return $Net::LDAP::ERROR;
} else {
return;
}
}
# error
#-----------------------------------------------------------------
#---------------------------------------------------------------
#---------------------------------------------------------------
# Code related to command-line stuff
use strict;
use Exporter;
use vars qw($UIDFILE @ISA @EXPORT $VERSION);
@ISA = qw(Exporter);
@EXPORT = qw(
CLIauth
);
$VERSION = 2.00;
$UIDFILE = $ENV{'LDAP_UIDFILE'} || glob("~/.ldapuids");
#-----------------------------------------------------------------
#-----------------------------------------------------------------
#-----------------------------------------------------------------
# CLIauth
# command-line authentication routine
sub CLIauth {
debug("Entering CLIauth");
use Term::ReadKey;
use Net::LDAP;
#my ($pass,$dn,$uid,$UIDFILE,$active,$tmp,$server,$base,$tmpdn,$line);
#my (%hash,$config->ldap'},$results,%args,$default);
my (%args,$config,@clist,$tmp,$source,$var,$results,$active,$uid);
my (%dns);
if (@_) {
$config = Net::LDAP::Config->new(@_) or die "Could not retrieve config\n";
}
# now we either have a server list or a defined source
# now we need to try to get the user's login
# retrieve the uids
my (%uids,%cuids);
%uids = getUids();
# cache the existing uids, for later comparison, so we don't rewrite
# the file unless it's changed
%cuids = %uids;
unless ($config->dn()) {
if ($config->source()) {
debug("source is " . $config->source());
if (exists $uids{$config->source()}) {
$config->dn($uids{$config->source()});
}
}
debug("looking in servers for uid");
if ($config->servers()) {
foreach (@{ $config->servers() }) {
if (exists $uids{$_} and $uids{$_}) {
debug("uid from $_");
$config->dn($uids{$_});
last;
}
}
}
}
# see if they passed one and not the other...
if (! $config->dn() && $config->uid()) {
$config->dn($config->uid());
}
print $config->dn(), "\n";
# this tells whether they are piping to us or have an interactive session
if (-t STDIN) {
$active = '1';
} else {
$active = '0';
}
# no point in prompting if it's not interactive
if ($active) {
open INPUT, "/dev/tty";
open OUTPUT, ">/dev/tty";
while (! $config->dn()) {
print OUTPUT "Username: ";
#$uid = <INPUT>;
#chomp $uid;
$tmp = <INPUT>;
chomp $tmp;
$config->dn($tmp);
}
while (! $config->password()) {
print OUTPUT "password: ";
ReadMode('noecho');
$tmp = <INPUT>;
chomp $tmp;
$config->password($tmp);
ReadMode('normal');
print OUTPUT "\n";
}
# if $config->uid() and $config->dn() disagree see if they want to overwrite .uid
if (
$config->uid() &&
($config->dn() ne $config->uid()) &&
($UIDFILE && -f $UIDFILE)
) {
print OUTPUT "Overwrite $UIDFILE? (y/[n]) ";
chomp ($tmp = <INPUT>);
}
close INPUT;
close OUTPUT;
} else {
if (! ( $config->dn() && $config->password()) ) {
error("You must provide both a uid and a password.");
exit(1);
}
}
#unless ($config->dn() =~ /^uid=/)
unless ($config->dn() =~ /^[a-z]+=/) {
debug("dn not found...");
$config->connect() or
error("Could not connect to LDAP server " . $config->{'servers'}[0]), return;
$config->filter("(uid=" . $config->dn() . ")");
$results = $config->search();
$results->code and error("CLIauth: ", $results->error()), return;
if (my $entry = $results->pop_entry) {
$config->dn($entry->dn() );
} else {
error("CLIauth: Could not find user" . $config->dn());
return;
}
}
my $ldap;
until ($ldap = $config->ldap()) {
debug("have all the info now...");
$config->connect() or
error("Could not connect to LDAP server " . $config->server()) && return;
}
$results = $ldap->bind($config->dn(),'password' => $config->password());
$results->code and
error("Invalid username (" . $config->dn(). ") or password.") && return;
$config->ldap($ldap);
# now we have successfully connected, so we know we have a valid DN
# let's set it everywhere we can
if ($config->source()) {
#debug("setting uid for source");
$uids{$config->source()} = $config->dn();
}
foreach (@{ $config->servers() }) {
#debug("setting uid for $_");
$uids{$_} = $config->dn();
}
# if they want to overwrite, or if they don't have the file, try to create it
if (
(
(
( $tmp &&
($tmp =~ /^y/)
) ||
(! -f $UIDFILE)
) &&
$< != 0
) ||
join("", sort %uids) ne join("", sort %cuids)
)
{
debug("writing uids");
writeUids(%uids);
}
return $config;
}
# CLIauth
#-----------------------------------------------------------------
#-----------------------------------------------------------------
# getUids
sub getUids {
my (%uids,$line);
if ($ENV{'HOME'}) {
if (-f $UIDFILE) {
open UID, "$UIDFILE" or do {
error("Cannot read $UIDFILE; ignoring");
next;
};
while ($line = <UID>) {
my ($tmp1, $tmp2) = split /: /, $line;
chomp $tmp2;
$uids{$tmp1} = $tmp2;
}
close UID;
}
}
return %uids;
}
# getUids
#-----------------------------------------------------------------
#-----------------------------------------------------------------
# writeUids
sub writeUids {
my %uids = @_;
if (open UID, "> $UIDFILE") {
foreach (keys %uids) {
print UID "$_: $uids{$_}\n";
}
close UID;
} else {
error("Cannot overwrite $UIDFILE; skipping.");
return;
}
}
# writeUids
#-----------------------------------------------------------------
#---------------------------------------------------------------
#---------------------------------------------------------------
#---------------------------------------------------------------
#---------------------------------------------------------------
# stuff related to actually connecting to the server
#-----------------------------------------------------------------
# multiConnect
=item multiConnect
Connects to the first viable ldap server from a list or reference to
a list.
=cut
sub multiConnect {
use Net::LDAP;
debug("entering multiConnect");
my ($ldap,@list,$host,%args,$sslcan,$ssl,$config,$source);
if (ref $_[0] and ref $_[0] eq 'Net::LDAP::Config') {
$config = shift;
} else {
%args = @_;
# okay, see if we have a valid config...
$config = Net::LDAP::Config->new(%args) or die "Invalid config.\n";
}
#map {print "$_ => $args{$_}\n"; } keys %args;
unless ($config->servers() ) {
$config->error("Failed to acquire a list of servers.");
return;
}
@list = @{ $config->servers() };
unless (@list) { error("No server list") && return; }
debug("server list is [@list]");
unless ($config->ssl()) {
$config->ssl('none');
}
if (eval { require Net::LDAPS; } and ! $@)
{
debug("ssl capable");
$sslcan = 1;
} else {
# nothing...
}
for ($config->ssl) {
/require/i and do {
unless ($sslcan) {
error("ssl is required but not possible");
return;
}
$ssl = 1;
next;
};
/prefer/i and do {
if ($sslcan) {
$ssl = 1;
}
next;
};
/none/i and do {
$ssl = 0;
next;
};
if ($sslcan) { $ssl = 1; }
}
#debug("ssl is $ssl");
while (@list and ! $ldap) {
$host = shift @list;
if ($ssl and $sslcan) {
debug("using ssl");
$ldap = Net::LDAPS->new($host,) or next;
} else {
$ldap = Net::LDAP->new($host,) or next;
}
}
if ($ldap) {
$config->ldap($ldap);
return $config;
=begin comment
if (wantarray)
{
return (%$config);
}
else
{
return $ldap;
}
=cut
} else {
return;
}
}
# multiConnect
#-----------------------------------------------------------------
#-----------------------------------------------------------------
# servers
=item servers
Allows developers to pick from a list of configured hosts,
or to get the list.
=cut
sub serverlist {
unless ($Net::LDAP::Config::SERVERS) {
die "Net::LDAP::Connect is not configured yet; either edit the
file manually, or run Net::LDAP::Connect::config.\n";
}
my (@return,$server);
foreach $server (@_) {
if (exists $Net::LDAP::Config::SERVERS->{$server} ) {
push @return, $Net::LDAP::Config::SERVERS->{$server};
}
}
if (@return) {
if (wantarray) {
return @return;
} else {
return shift @return;
}
} else {
if (wantarray) {
return keys %$Net::LDAP::Config::SERVERS;
}
}
}
# servers
#-----------------------------------------------------------------
#---------------------------------------------------------------
#---------------------------------------------------------------
# and here's the actual config code
#---------------------------------------------------------------
# AUTOLOAD
# until i see a reason to do it otherwise, I'm just going to autoload
# everything...
sub AUTOLOAD {
my $func = &_compile;
goto &$func;
}
# AUTOLOAD
#---------------------------------------------------------------
#---------------------------------------------------------------
# _compile
sub _compile {
use vars qw($TEXT);
$TEXT ||=
q[
my $config = shift;
if (@_) {
$config->{$var} = shift;
}
if (wantarray and ref $config->{$var} eq 'ARRAY') {
return @{ $config->{$var} };
} elsif (wantarray and ref $config->{$var} eq 'HASH') {
return %{ $config->{$var} };
} else {
return $config->{$var};
}
];
my ($func,$pack,$func_name);
$func = $AUTOLOAD;
$func=~/(.+)::([^:]+)$/;
($pack,$func_name) = ($1,$2);
if ($pack ne 'Net::LDAP::Config') {
die "Cannot AUTOLOAD outside of Net::LDAP::Config\n";
}
eval
"sub $func_name
{
my \$var = '$func_name';
$TEXT
}";
return $func_name;
}
# _compile
#---------------------------------------------------------------
#---------------------------------------------------------------
# bind
sub bind {
my $obj = shift;
my $ldap;
unless ($ldap = $obj->ldap()) {
$obj->connect() or die "Could not connect to LDAP\n";
$ldap = $obj->ldap();
}
my %args;
if (@_) {
%args = @_;
}
unless ($obj->anonymous()) {
if (my $dn = $obj->dn()) {
$args{'dn'} ||= $dn;
}
if (my $password = $obj->password()) {
$args{'password'} ||= $password;
}
}
$obj->{'bind'}++;
return $obj->ldap()->bind(%args);
}
# bind
#---------------------------------------------------------------
#---------------------------------------------------------------
# clauth
sub clauth {
my $obj = shift;
$obj->debug("calling CLIauth");
my $config = CLIauth($obj) || die error();
$obj->debug("config is $config");
$obj->{'connected'}++;
return $config;
}
# clauth
#---------------------------------------------------------------
#---------------------------------------------------------------
# connect
sub connect {
my $obj = shift;
$obj->debug("calling multiConnect");
if (my $config = multiConnect($obj)) {
$obj->debug("config is $config");
$obj->{'connected'}++;
return $config;
} else {
warn $config->error, "\n";
exit;
}
}
# connect
#---------------------------------------------------------------
#---------------------------------------------------------------
sub loadconfig {
my ($config,$ref) = @_;
unless (-e $config) {
die "You must create the config, currently set to: \n\t$config\n";
}
open CONFIG, $config or
die "Could not open $config: $!\n";
my ($group,$lineno);
while (my $line = <CONFIG>) {
$lineno++;
for ($line) {
/^#/ and do {
next;
};
/^\s*$/ and do {
next;
};
/^\[*(.+)\]/ and do {
$group = $1;
next;
};
/^([^:]+):\s+(.+)/ and do {
unless ($group) {
die "Invalid line at line $lineno:\n$line";
}
#warn "setting $1 to [$2] in $group\n";
$ref->{$group}->{$1} = $2;
next;
};
die "Invalid line in $config at line $lineno:\n$line";
}
}
close CONFIG;
}
# loadconfig
#---------------------------------------------------------------
#---------------------------------------------------------------
sub init {
# currently if all of these exist, they'll all be loaded; that's
# probably okay...
# the possible main configs
my @mains;
if ($_[0]) {
push @mains, $_[0];
}
push @mains, "/etc/ldapsh_config", "/usr/local/etc/ldapsh_config";
# the possible personal configs
my @personals;
if ($_[0]) {
push @personals, $_[0];
}
push @personals, glob("~/.ldapsh_config");
my %hash;
my $loaded = 0;
foreach my $config (@mains, @personals) {
next unless $config;
if (-e $config) {
debug "loading $config\n";
loadconfig($config,\%hash);
$loaded++;
} else {
debug "No file $config\n";
}
}
unless ($loaded) {
warn "Could not find a configuration file. Please create one of:\n\t" .
join("\n\t",@mains,@personals) . "\n";
exit(14);
}
# set up our default source
if (exists $hash{'main'} and exists $hash{'main'}->{'default'}) {
my $default = $hash{'main'}->{'default'};
debug "default is $default\n";
unless (exists $hash{$default}) {
die "Could not find default source '$default'\n";
}
$hash{'default'} = $hash{$default};
}
delete $hash{'main'};
# now fix the server stuff
foreach my $source (keys %hash) {
next if $source eq 'default';
my $servers = $hash{$source}->{'server'} ||
$hash{$source}->{'servers'} ||
"";
delete $hash{$source}->{'server'};
delete $hash{$source}->{'servers'};
my (@servers,$pattern);
if ($servers =~ /\s/) {
@servers = split /\s/, $servers;
} elsif ($servers =~ /,/) {
@servers = split /,/, $servers;
} else {
# this should only be one server
push @servers, $servers;
#@servers = ($servers);
}
unless (@servers) {
warn "No servers defined for source '$source'; skipping\n";
delete $hash{$source};
next;
}
$hash{$source}->{'servers'} = \@servers;
}
# this still just feels like a big hack, but that's probably okay...
$Net::LDAP::Config::SOURCES = \%hash;
return \%hash;
}
# init
#---------------------------------------------------------------
#---------------------------------------------------------------
# ldapsearch
sub ldapsearch {
my $obj = shift;
unless ($obj->ldap()) {
return;
}
return $obj->ldap()->search(@_);
}
# ldapsearch
#---------------------------------------------------------------
#---------------------------------------------------------------
# new
# build our new config, based on either what is configured in
# the Sources modules, or what is passed in
sub new {
my $class = shift;
if (ref $_[0] eq 'Net::LDAP::Config') {
return shift @_;
}
my $config = {};
bless $config, $class;
my ($source,%args,$var);
%args = @_;
# pull in the config file
# this is what allows us to specify a different config file
unless ($Net::LDAP::Config::SOURCES) {
my @initargs;
if (exists $args{'config'}) {
push @initargs, $args{'config'};
}
init(@initargs);
}
use subs;
# first pull in anything from the basic config
if ($args{'source'}) {
$source = $Net::LDAP::Config::SOURCES->{$args{'source'}} or die
"Source '$args{source}' could not be found. Please configure
Net::LDAP::Sources appropriately.\n";
# we just want to call the init for all known routines
# it should be set up so that the variables stored also
# have routines with the same name
foreach $var (keys %$source) {
#print "working on $var\n";
my $value = eval { $config->$var($source->{$var}); };
#print "value is $value from $source->{$var}\n";
if ($@) {
die "Option '$var' not valid.\n";
}
}
}
# then do any overrides based on stuff passed in
foreach $var (keys %args) {
eval { $config->$var($args{$var}); };
if ($@) {
die "Option '$var' not valid.\n";
}
}
#if ($args{'bind') {
# $config->bind();
#}
# okay, at this point, we theoretically have a complete
# config
return $config;
}
# new
#---------------------------------------------------------------
#---------------------------------------------------------------
# search
sub search {
my $obj = shift;
unless ($obj->ldap()) {
$obj->connect();
}
my %args = @_;
my %hash;
# we actually want to allow a null search base
$hash{'base'} = $args{'base'} || $obj->base() || "";
#unless ($hash{'base'} = $args{'base'} || $obj->base()) {
# warn "LDAP Search base is unset\n";
# return;
#}
unless ($hash{'filter'} = $args{'filter'} || $obj->filter()) {
warn "LDAP Search filter is unset\n";
return;
}
unless ($hash{'attrs'} = $args{'attrs'} || $obj->attrs()) {
delete $hash{'attrs'};
}
return $obj->ldapsearch(%hash);
}
# search
#---------------------------------------------------------------
# $Id: Config.pm,v 1.4 2004/07/26 22:33:08 luke Exp $
1;