File Coverage

blib/lib/Config/LDAPClient.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Config::LDAPClient;
2              
3 1     1   1349 use Moose;
  0            
  0            
4             use Moose::Util::TypeConstraints;
5             use MooseX::AttributeHelpers;
6             use Config::LDAPClient::Search;
7             use Carp qw();
8             use warnings;
9             use strict;
10              
11              
12             our $VERSION = '0.01';
13              
14              
15             my $PAM_SECRET_KEY = 'rootbindpw';
16              
17              
18             # Here for future support.
19             #has 'c_scope' => ( is => 'rw', isa => enum([qw/ sub one base /]));
20             #has 'c_deref' => ( is => 'rw', isa => enum([qw/ never search find always /]));
21             #has 'c_ssl_key' => ( is => 'rw', isa => 'Str' );
22             #has 'c_ssl_cert' => ( is => 'rw', isa => 'Str' );
23              
24             has 'c_ssl_type' => ( is => 'rw', isa => enum([qw/ off ssl tls /]));
25             has 'c_ssl_verify' => ( is => 'rw', isa => enum([qw/ none optional require /]));
26              
27             has 'c_ssl_capath' => ( is => 'rw', isa => 'Str' );
28             has 'c_ssl_cafile' => ( is => 'rw', isa => 'Str' );
29             has 'c_ldap_version' => ( is => 'rw', isa => 'Int' );
30             has 'c_bind_dn' => ( is => 'rw', isa => 'Str' );
31             has 'c_bind_password' => ( is => 'rw', isa => 'Str' );
32             has 'c_base' => ( is => 'rw', isa => 'Str' );
33             has 'c_search_passwd' => ( is => 'rw', isa => 'Config::LDAPClient::Search' );
34             has 'c_search_group' => ( is => 'rw', isa => 'Config::LDAPClient::Search' );
35             has 'c_search_shadow' => ( is => 'rw', isa => 'Config::LDAPClient::Search' );
36              
37             has 'c_uri' => (
38             is => 'rw',
39             isa => 'ArrayRef[Str]',
40             default => sub { [] },
41             auto_deref => 1,
42             );
43              
44             has 'raw_configs' => ( is => 'rw', isa => 'HashRef', default => sub {{}} );
45             has 'parsed' => ( is => 'rw', isa => 'HashRef', default => sub {{}} );
46              
47             has 'diag' => (
48             metaclass => 'Collection::Array',
49             is => 'ro',
50             isa => 'ArrayRef[Str]',
51             default => sub { [] },
52             auto_deref => 1,
53             provides => {
54             push => 'add_diag',
55             clear => 'clear_diag',
56             },
57             );
58              
59             __PACKAGE__->meta->make_immutable;
60              
61              
62              
63              
64             sub debug {
65             my($self) = shift;
66             my $message = join "", @_;
67             $self->add_diag($message);
68             }
69              
70              
71              
72              
73             sub connect {
74             my($self, %args) = @_;
75             my $new = $args{'new'} || {};
76             Carp::croak("'new' argument must be a hashref")
77             unless ref $new eq 'HASH';
78              
79             require Net::LDAP;
80              
81             my $ldap = Net::LDAP->new(
82             [$self->c_uri],
83             onerror => 'die',
84             version => $self->c_ldap_version,
85             %$new,
86             );
87              
88             Carp::croak("Unable to connect to LDAP: $@") unless $ldap;
89              
90              
91             my $bind_dn = $self->c_bind_dn;
92             my @bind_args;
93             if (defined $bind_dn) {
94             my $pw = $self->c_bind_password;
95             push @bind_args, $bind_dn;
96             push @bind_args, password => $pw if defined $pw;
97             }
98              
99             {
100             my $mesg = $ldap->bind(@bind_args);
101             Carp::croak("Unable to bind LDAP connection: ", $mesg->error)
102             if $mesg->is_error;
103             }
104              
105              
106             my $ssl = $self->c_ssl_type;
107             if ($ssl ne 'off') {
108             my $mesg = $ldap->start_tls(
109             verify => $self->c_ssl_verify,
110             capath => $self->c_ssl_capath,
111             cafile => $self->c_ssl_cafile,
112             sslversion => $ssl eq 'tls' ? 'tlsv1' : 'sslv3',
113             );
114              
115             Carp::croak("Unable to start TLS on LDAP connection: ", $mesg->error)
116             if $mesg->is_error;
117             }
118              
119             return $ldap;
120             }
121              
122              
123              
124              
125             sub parse {
126             my $self = shift;
127             my @specs = $self->_validate_specs(@_);
128              
129             my %raw_configs;
130             my %final_parsed;
131             $self->clear_diag;
132              
133             # The user specifies specs in priority order; first overrides second, etc.
134             # So we reverse them, and let the hashes fall out as necessary.
135              
136             foreach my $spec (reverse @specs) {
137             my($configs, $secret, $parse) = @$spec;
138              
139             foreach my $config (@$configs) {
140             my($parsed, $raw);
141             my $success = eval {
142             ($parsed, $raw) = $self->$parse($config, $secret);
143             1;
144             };
145              
146             if ($success) {
147             $raw_configs{$config} = $raw;
148              
149             my($dn, $dnpw) = delete @{$parsed}{'bind_dn', 'bind_password'};
150             if (defined $dn and length $dn) {
151             # These values are associated, so we only assign both if
152             # we have a DN to use.
153             $final_parsed{'bind_dn'} = $dn;
154             $final_parsed{'bind_password'} = $dnpw;
155             }
156              
157             while (my($k, $v) = each %$parsed) {
158             $final_parsed{$k} = $v
159             if defined $v and length $v;
160             }
161              
162             } else {
163             $self->debug($@ || "unknown error parsing '$config'");
164             }
165             }
166             }
167              
168             $self->raw_configs(\%raw_configs);
169             $self->parsed (\%final_parsed);
170              
171             foreach my $key (keys %final_parsed) {
172             my $method = $self->can("c_$key");
173             Carp::croak("Unknown parsed key '$key' found") unless $method;
174             $method->($self, $final_parsed{$key});
175             }
176              
177             return $self;
178             }
179              
180              
181              
182              
183             my %SPECS = (
184             pam => {
185             config => '/etc/pam_ldap.conf',
186             secret => '/etc/pam_ldap.secret',
187             parse => 'parse_file_pam',
188             },
189              
190             nss => {
191             config => '/etc/libnss-ldap.conf',
192             secret => '/etc/libnss-ldap.secret',
193             parse => 'parse_file_nss',
194             },
195              
196             pam_nss => {
197             config => '/etc/ldap.conf',
198             secret => '/etc/ldap.secret',
199             parse => 'parse_file_nss',
200             },
201              
202             libldap => {
203             config => '/etc/ldap/ldap.conf',
204             parse => 'parse_file_libldap',
205             },
206              
207             libldap_home => {
208             config => ["$ENV{HOME}/.ldaprc", "$ENV{HOME}/ldaprc"],
209             parse => 'parse_file_libldap',
210             },
211             );
212              
213             sub _validate_specs {
214             my $self = shift;
215              
216             my @specs;
217             foreach my $reqspec (@_) {
218             my $spec;
219             if (not ref $reqspec) {
220             $spec = $SPECS{$reqspec};
221             Carp::croak("Unknown parsing specification name '$reqspec'")
222             unless $spec;
223              
224             } elsif (ref $reqspec eq 'HASH') {
225             $spec = $reqspec;
226              
227             } else {
228             Carp::croak(
229             "Unknown parsing specification reference '", ref $reqspec, "'"
230             );
231             }
232              
233             my($configs, $secret, $parse) = @{$spec}{qw( config secret parse )};
234             $configs = [$configs] unless ref $configs eq 'ARRAY';
235             $parse = ref $parse eq 'CODE' ? $parse : $self->can($parse);
236              
237             {
238             my $err;
239              
240             $err = "'parse' key must be a subref or valid method name"
241             unless ref $parse eq 'CODE';
242              
243             $err = "no 'parse' key"
244             unless defined $parse;
245              
246             $err = "no configuration files specified"
247             unless grep { defined } @$configs;
248              
249             if ($err) {
250             require Data::Dumper;
251             my $safespec = Data::Dumper->new([$reqspec])
252             ->Terse(1)->Indent(0)->Useqq(1)->Dump;
253              
254             Carp::croak("Invalid parsing specification $safespec: $err");
255             }
256             }
257              
258             push @specs, [$configs, $secret, $parse];
259             }
260              
261             return @specs;
262             }
263              
264              
265              
266              
267             sub _parse_lokv_file {
268             # Parses a simple line-oriented, key-value pair file format. Each line has
269             # the name of a setting, followed by whitespace and the value. Any line
270             # starting wth #, with any amount of leading whitespace, is treated as a
271             # comment and ignored. Blank lines are ignored.
272             #
273             # Returns a hashref of the parsed key-value pairs.
274              
275             my($self, $file) = @_;
276              
277             open(my $conffh, '<', $file)
278             || die("Unable to open configuration file '$file': $!.\n");
279              
280             my %config;
281             while (my $line = <$conffh>) {
282             next if $line =~ /^\s*#/ or $line !~ /\S/;
283              
284             chomp $line;
285             my($key, $value) = split " ", $line, 2;
286              
287             Carp::carp("Duplicate keys '$key' in file '$file'")
288             if exists $config{$key};
289            
290             $config{$key} = $value;
291             }
292              
293             close $conffh;
294              
295             return \%config;
296             }
297              
298              
299              
300              
301             sub _parse_hosts_uris {
302             my($self, $port, $hosts, $uris) = @_;
303              
304             if ($uris) {
305             return split " ", $uris;
306              
307             } elsif ($hosts) {
308             my @uri;
309             foreach my $host (split " ", $hosts) {
310             my $uri = "ldap://$host";
311             $uri .= ":$port" if $port and $host !~ /:/;
312             push @uri, $uri;
313             }
314             return @uri;
315             }
316              
317             return;
318             }
319              
320              
321              
322              
323             my %LIBLDAP_REQCERT = (
324             never => 'none',
325             allow => 'optional',
326             try => 'optional',
327             demand => 'require',
328             hard => 'require',
329             );
330              
331             sub parse_file_libldap {
332             my($self, $conf_filename) = @_;
333             my %config = %{ $self->_parse_lokv_file($conf_filename) };
334              
335             my %parsed = (
336             uri => [ $self->_parse_hosts_uris(@config{qw/ PORT HOST URI /}) ],
337             bind_dn => $config{'BIND_DN'},
338             ssl_capath => $config{'TLS_CACERTDIR'},
339             ssl_cacert => $config{'TLS_CACERT'},
340             ssl_verify => $LIBLDAP_REQCERT{ $config{'TLS_REQCERT'} || 'allow' },
341             );
342              
343             return (\%parsed, \%config);
344             }
345              
346              
347              
348              
349             sub parse_file_nss {
350             my $self = shift;
351             my($parsed, $config) = $self->parse_file_pam(@_);
352              
353             foreach my $map (qw( passwd group shadow )) {
354             if ($config->{"nss_base_$map"}) {
355             $parsed->{"search_$map"} = Config::LDAPClient::Search->new(
356             split /\?/, $config->{"nss_base_$map"}, 3
357             );
358             }
359             }
360              
361             return ($parsed, $config);
362             }
363              
364              
365              
366              
367             sub parse_file_pam {
368             my($self, $conf_filename, $secret_filename) = @_;
369             my %config = %{ $self->_parse_lokv_file($conf_filename) };
370              
371             if (defined $secret_filename) {
372             if (open my $secretfh, '<', $secret_filename) {
373             Carp::carp("Config file '$conf_filename' has a '$PAM_SECRET_KEY' key already")
374             if exists $config{$PAM_SECRET_KEY};
375              
376             chomp($config{$PAM_SECRET_KEY} = <$secretfh>);
377             close $secretfh;
378              
379             } else {
380             $self->debug("Unable to open secret file '$secret_filename': $!.");
381             }
382             }
383              
384              
385             my($dn, $dnpw) = $self->_process_pam_dn(\%config);
386             my %parsed = (
387             uri => [ $self->_parse_hosts_uris(@config{qw( port host uri )}) ],
388             ldap_version => $config{'ldap_version'},
389             bind_dn => $dn,
390             bind_password => $dnpw,
391             port => $config{'port'},
392             base => $config{'base'},
393             ssl_capath => $config{'tls_cacertdir'},
394             ssl_cafile => $config{'tls_cacertfile'},
395             ssl_type => $self->_process_pam_ssl($config{'ssl'}),
396             ssl_verify =>
397             lc $config{'tls_checkpeer'} eq 'yes'
398             ? 'require'
399             : 'optional',
400             );
401              
402              
403             return (\%parsed, \%config);
404             }
405              
406              
407              
408             my %SSL_TYPES = qw( on ssl start_tls tls off off );
409             sub _process_pam_ssl { $SSL_TYPES{ $_[1] || 'off' } }
410              
411              
412              
413              
414             sub _process_pam_dn {
415             my($self, $config) = @_;
416              
417             my $root_dn = $config->{'rootbinddn'};
418             my $root_pw = $config->{'rootbindpw'};
419              
420             if ($root_dn and defined $root_pw) {
421             return ($root_dn, $root_pw);
422             } else {
423             return ($config->{'binddn'}, $config->{'bindpw'});
424             }
425             }
426              
427              
428              
429              
430             1;
431              
432             __END__
433              
434             =head1 NAME
435              
436             Config::LDAPClient - parse system configuration for LDAP client settings.
437              
438              
439             =head1 SYNOPSIS
440              
441             use Config::LDAPClient;
442              
443             my $conf = Config::LDAPClient->new();
444             $conf->parse(
445             'pam', 'nss', 'libldap',
446             { config => '/etc/custom-ldap.conf', parse => \&custom_ldap_parser },
447             );
448              
449             print "hosts: ", join(" ", $conf->c_uri), "\n";
450              
451             my $ldap = $conf->connect;
452             # Call Net::LDAP methods on $ldap.
453              
454             sub custom_ldap_parser { ... }
455              
456              
457             =head1 DESCRIPTION
458              
459             *** WARNING *** This is very much alpha software. Testing has been minimal,
460             and the API is somewhat subject to change.
461              
462             On many systems there is existing configuration for how to connect to an LDAP
463             server, usually in order to perform authentication for the system itself.
464             This module reads that configuration, parses it, and presents a common
465             interface that can then be used to connect to the specified LDAP server.
466              
467             For a list of configuration files supported see L</Configuration Files>.
468              
469              
470             =head2 Methods
471              
472             All methods raise exceptions on errors. Currently these are simply string
473             exceptions.
474              
475             =over 4
476              
477             =item $class->new( ... )
478              
479             This is the class's constructor. It takes a hashref, or a list of key-value
480             pairs; these are treated as method names, and the methods are called with the
481             associated values. This method is supplied by Moose.
482              
483              
484             =item $object->connect
485              
486             =item $object->connect( new => \%args )
487              
488             Attempts to connect to an LDAP database using L<Net::LDAP>. Attributes should
489             be set to appropriate values, which means L</parse> probably should be called
490             first.
491              
492             The C<new> argument, if specified, must be a hashref. It is dereferenced and
493             passed to the Net::LDAP constructor. It is used to override any default
494             options set by L<Config::LDAPClient>, and any L<Net::LDAP> defaults.
495              
496             Currently the only default constructor argument specified by
497             L<Config::LDAPClient> is C<onerror>, which is set to 'die'.
498              
499              
500             =item $object->parse( @names_or_specifications )
501              
502             This is the workhorse of the module. The arguments to this method are a
503             series of pre-defined names and/or hashrefs indicating what configuration
504             files to read, and how to parse them.
505              
506             Pre-defined names are listed in L</Configuration Files>.
507              
508             If a hashref is specified, it must contain at least two keys, C<config> and
509             C<parse>. C<config> is a scalar or arrayref listing the configuration files
510             to read; C<parse> is the method or subroutine reference to call. An
511             additional parameter, C<secret>, may be provided; this is the name of the
512             file that contains the bind password required to connect. Any problems
513             opening this file are not fatal, and will be added to C<diag>, but otherwise
514             ignored.
515              
516             The C<parse> subroutine or method is expected to return two values: a hashref
517             of the parsed values, to be passed to C<c_*> methods, and a data structure
518             representing the raw parsed configuration. The C<parse> subroutine or method
519             is called with three arguments: the Config::LDAPClient object, the
520             configuration filename or names, and optionally the secret filename (if it's
521             specified).
522              
523             Names and specifications are listed in priority order, meaning the first file
524             found takes precedence over subsequent files. All settings are merged, with
525             the highest priority taking precedence.
526              
527              
528             =item $object->parse_file_pam($conf_filename, $secret_filename)
529              
530             This parses the PAM LDAP configuration file format. The parsing, and
531             subsequent handling of options, is based on a reading of the pam_ldap.conf(5)
532             man page from libpam-ldap 184-4.2 installed on Debian Lenny.
533              
534             This method conforms to the description of the C<parse> argument described in
535             the L<"parse method"/parse> documentation.
536              
537              
538             =item $object->parse_file_nss($conf_filename, $secret_filename)
539              
540             This parses the NSS LDAP configuration file format. It first callse
541             L</parse_file_pam>, because the formats and most of the options are
542             identical, then does specific handling. The handling of options ceomes from
543             a reading of the libnss-ldap.conf(5) man page from libnss-ldap 261-2.1
544             installed on Debian Lenny.
545              
546             This method conforms to the description of the C<parse> argument described in
547             the L<"parse method"/parse> documentation.
548              
549              
550             =item $object->parse_file_libldap($conf_filename)
551              
552             This parses the libldap configuration file format. The parsing and handling
553             of options comes from a reading of the ldap.conf(5) man page from
554             libldap-2.4-2 installed on Debian Lenny.
555              
556             This method conforms to the description of the C<parse> argument described in
557             the L<"parse method"/parse> documentation.
558              
559              
560             =item $object->diag
561              
562             =item $object->add_diag($message)
563              
564             =item $object->clear_diag
565              
566             The C<diag> method accesses an array of non-fatal errors encountered in a
567             given L</parse> run. C<add_diag> adds an entry, and C<clear_diag> clears the
568             entire array.
569              
570              
571             =item $object->raw_configs
572              
573             =item $object->raw_configs(\%configs)
574              
575             Accessor for the raw configuration data parsed from files. The hash keys are
576             the filenames, the values the configuration data returned from the parse.
577              
578              
579             =item $object->parsed
580              
581             =item $object->parsed(\%parsed)
582              
583             Accessor for the parsed and processed data. This is all of the original
584             merged data, and should correspond directly to the values returned by the
585             C<c_*> accessors.
586              
587             =back
588              
589              
590             =head2 Configuration Accessors
591              
592             =over 4
593              
594             =item $object->c_uri
595              
596             =item $object->c_uri(\@uris)
597              
598             Accessor for URIs to connect to. Corresponds to the HOST argument for the
599             new method in L<Net::LDAP>. Returns a list of URIs, not an arrayref.
600              
601              
602             =item $object->c_ldap_version
603              
604             =item $object->c_ldap_version($number)
605              
606             Accessor for the LDAP protocol version to be used. Corresponds to the
607             version argument to the L<Net::LDAP> constructor.
608              
609              
610             =item $object->c_bind_dn
611              
612             =item $object->c_bind_dn($dn)
613              
614             Accessor for the DN to bind to use on connect. Corresponds to the first
615             argument to the bind method in L<Net::LDAP>.
616              
617              
618             =item $object->c_bind_password
619              
620             =item $object->c_bind_password($password)
621              
622             Accessor for the bind password to use on connect. Corresponds to the
623             password argument to the bind method in L<Net::LDAP>.
624              
625              
626             =item $object->c_base
627              
628             =item $object->c_base($base)
629              
630             Accessor for the default base DN to use in searches.
631              
632              
633             =item $object->c_ssl_type
634              
635             =item $object->c_ssl_type($type)
636              
637             Accessor for the SSL access type; valid values are 'off', 'ssl', or 'tls'.
638              
639              
640             =item $object->c_ssl_verify
641              
642             =item $object->c_ssl_verify($verify)
643              
644             Accessor for the SSL verification requirement; valid values correspond to
645             the verify argument to start_tls in L<Net::LDAP>, namely 'none', 'optional',
646             and 'require'.
647              
648              
649             =item $object->c_ssl_capath
650              
651             =item $object->c_ssl_capath($path)
652              
653             Accessor for the directory of CA certificates. Corresponds to the capath
654             argument to start_tls in L<Net::LDAP>.
655              
656              
657             =item $object->c_ssl_cafile
658              
659             =item $object->c_ssl_cafile($filename)
660              
661             Accessor for the CA certificates file. Corresponds to the cafile argument to
662             start_tls in L<Net::LDAP>.
663              
664              
665             =item $object->c_search_passwd
666              
667             =item $object->c_search_passwd($object)
668              
669             =item $object->c_search_group
670              
671             =item $object->c_search_group($object)
672              
673             =item $object->c_search_shadow
674              
675             =item $object->c_search_shadow($object)
676              
677             These apply specifically to NSS. They are accessors for base DNs to use for
678             specific lookups. The object used is an L<Config::LDAPClient::Search>
679             object, or subclass thereof.
680              
681             =back
682              
683              
684             =head2 Configuration Files
685              
686             This module comes with several pre-defined paths that it can attempt to
687             parse. These names can be passed directly to L</parse>.
688              
689             =over 4
690              
691             =item * pam
692              
693             Attempts to parse /etc/pam_ldap.conf and /etc/pam_ldap.secret, using
694             L</parse_file_pam>. The secret file is typically owned by root and mode
695             0600, so unless you run as root, you will get the binddn and bindpw values in
696             L</c_bind_dn> and L</c_bind_password>.
697              
698              
699             =item * nss
700              
701             Attempts to parse /etc/libnss-ldap.conf and /etc/libnss-ldap.secret, using
702             L</parse_file_nss>. The secret file is typically owned by root and mode
703             0600, so unless you run as root, you will get the binddn and bindpw values in
704             L</c_bind_dn> and L</c_bind_password>.
705              
706              
707             =item * pam_nss
708              
709             Some systems merge their libnss-ldap and pam-ldap configuration files. This
710             attempts to parse /etc/ldap.conf and /etc/ldap.secret. The secret file is
711             typically owned by root and mode 0600, so unless you run as root, you will
712             get the binddn and bindpw values in L</c_bind_dn> and L</c_bind_password>.
713              
714              
715             =item * libldap
716              
717             Attempts to parse /etc/ldap/ldap.conf using L</parse_file_libldap>.
718              
719              
720             =item * libldap_home
721              
722             Attempts to parse C<$ENV{HOME}/.ldaprc> and C<$ENV{HOME}/ldaprc>.
723              
724             =back
725              
726              
727             =head1 BUGS
728              
729             The test suite is non-existent.
730              
731             Option handling is not comprehensive. Not all of the options available in
732             pam_ldap.conf and libnss-ldap.conf are actually used, even though they have
733             equivalents in Net::LDAP.
734              
735             Currently this only supports common Linux setups (specifically Debian Lenny
736             and Ubuntu Hardy Heron). Support for more systems is forthcoming.
737              
738             In order to speed development time, this module uses Moose. This increases
739             the dependency list by a few orders of magnitude, which you may or may not
740             consider a bug.
741              
742             All of the configuration options available are specified as toplevel methods,
743             albeit with 'c_' prefixes. This could be considered a design bug, but it was
744             the simplest way to involve Moose's type checking.
745              
746             Aside from support and design, there are probably more than a few bugs lurking
747             about. This module was written quickly over a weekend with very minimal
748             testing, as the lack of a test suite can attest.
749              
750              
751             =head1 AUTHOR
752              
753             Michael Fowler <mfowler@cpan.org>
754              
755              
756             =head1 COPYRIGHT & LICENSE
757              
758             Copyright 2009 Michael Fowler
759              
760             This program is free software; you can redistribute it and/or
761             modify it under the terms of either:
762              
763             =over 4
764              
765             =item * the GNU General Public License as published by the Free
766             Software Foundation; either version 1, or (at your option) any
767             later version, or
768              
769             =item * the Artistic License version 2.0.
770              
771             =back
772              
773             =cut