File Coverage

blib/lib/Dancer/Plugin/LDAP/Handle.pm
Criterion Covered Total %
statement 18 220 8.1
branch 0 96 0.0
condition 0 28 0.0
subroutine 6 22 27.2
pod 13 13 100.0
total 37 379 9.7


line stmt bran cond sub pod time code
1             package Dancer::Plugin::LDAP::Handle;
2              
3 1     1   4 use strict;
  1         2  
  1         35  
4 1     1   5 use Carp;
  1         2  
  1         57  
5 1     1   5 use Net::LDAP;
  1         2  
  1         5  
6 1     1   3381 use Net::LDAP::Util qw(escape_dn_value escape_filter_value ldap_explode_dn);
  1         66  
  1         77  
7 1     1   5 use Encode;
  1         2  
  1         88  
8              
9 1     1   6 use base qw(Net::LDAP);
  1         2  
  1         2290  
10              
11             our $VERSION = '0.0050';
12              
13             =head1 NAME
14              
15             Dancer::Plugin::LDAP::Handle - subclassed Net::LDAP object
16              
17             =head1 SYNOPSIS
18              
19             =cut
20              
21             =head1 METHODS
22              
23             =cut
24              
25             =head2 quick_select
26              
27             quick_select performs a search in the LDAP directory.
28              
29             The simplest form is to just specify the filter:
30              
31             ldap->quick_select({objectClass => 'inetOrgPerson'});
32              
33             This retrieves all records of the object class C.
34              
35             A specific record can be fetched by using the distinguished name (DN)
36             as only key in the hash reference:
37              
38             ldap->quick_select({dn => 'uid=racke@linuxia.de,dc=linuxia,dc=de'});
39              
40             The base of your search can be passed as first argument, otherwise
41             the base defined in your settings will be used.
42              
43             ldap->quick_select('dc=linuxia,dc=de', {objectClass => 'inetOrgPerson'});
44              
45             You may add any options supported by the Net::LDAP search method,
46             e.g.:
47              
48             ldap->quick_select('dc=linuxia,dc=de', {objectClass => 'inetOrgPerson'},
49             scope => 'one');
50              
51             =head3 Attributes
52              
53             In addition, there is a C option which determines how values
54             for LDAP attributes are fetched:
55              
56             =over 4
57              
58             =item first
59              
60             First value of each attribute.
61              
62             =item last
63              
64             Last value of each attribute.
65              
66             =item asref
67              
68             Values as array reference.
69              
70             =back
71              
72             =cut
73              
74             sub quick_select {
75 0     0 1   my ($self) = shift;
76 0           my ($table, $spec_ref, $mesg, @conds, $filter, $key,
77             @search_args, @results, %opts, @ldap_args);
78              
79 0 0         if (ref($_[0]) eq 'HASH') {
80             # search specification is first argument
81 0           $table = $self->base();
82             }
83             else {
84 0           $table = shift;
85             }
86            
87 0           $spec_ref = shift;
88              
89             # check remaining parameters
90 0           %opts = (values => 'first');
91              
92 0           while (@_ > 0) {
93 0           $key = shift;
94            
95 0 0         if (exists $opts{$key}) {
96 0           $opts{$key} = shift;
97             }
98             else {
99 0           push(@ldap_args, $key, shift);
100             }
101             }
102              
103 0           @conds = $self->_build_conditions($spec_ref);
104              
105 0 0         if (@conds > 1) {
    0          
106 0           $filter = '(&' . join('', @conds) . ')';
107             }
108             elsif (exists $spec_ref->{dn}) {
109             # lookup of distinguished name
110 0           $filter = '(objectClass=*)';
111 0           $table = $spec_ref->{dn};
112 0           push (@_, scope => 'base');
113             }
114             else {
115 0           $filter = $conds[0];
116             }
117              
118             # compose search parameters
119 0           $table = $self->dn_escape($table);
120            
121 0           @search_args = (base => $table, filter => $filter, @_, @ldap_args);
122              
123 0           Dancer::Logger::debug('LDAP search: ', \@search_args);
124            
125 0           $mesg = $self->search(@search_args);
126              
127 0           foreach (my $i = 0; $i < $mesg->count; $i++) {
128 0           my $token = {};
129 0           my $entry = $mesg->entry($i);
130            
131 0           $token->{dn} = $self->_utf8_decode($self->dn_unescape($entry->dn));
132            
133 0           for my $attr ( $entry->attributes ) {
134 0 0         if ($opts{values} eq 'asref') {
    0          
135             # all attribute values as array reference
136 0           $token->{$attr} = [map {$self->_utf8_decode($_)} @{$entry->get_value($attr, asref => 1)}];
  0            
  0            
137             }
138             elsif ($opts{values} eq 'last') {
139             # last attribute value
140 0           my $value_ref = $entry->get_value($attr, asref => 1);
141 0 0         $token->{$attr} = defined($value_ref)
142             ? $self->_utf8_decode($value_ref->[-1])
143             : undef;
144             }
145             else {
146             # first attribute value
147 0           $token->{$attr} = $self->_utf8_decode($entry->get_value($attr));
148             }
149             }
150            
151 0           push(@results, $token);
152            
153             }
154              
155 0 0         if (wantarray) {
156 0           return @results;
157             }
158             else {
159 0           return $results[0];
160             }
161             }
162              
163             =head2 quick_insert $dn $ref %opts
164              
165             Adds an entry to LDAP directory.
166              
167             ldap->quick_insert('uid=racke@linuxia.de,ou=people,dc=linuxia,dc=de',
168             {cn => 'racke@linuxia.de',
169             uid => 'racke@linuxia.de',
170             givenName = 'Stefan',
171             sn => 'Hornburg',
172             c => 'Germany',
173             l => 'Wedemark',
174             objectClass => [qw/top person organizationalPerson inetOrgPerson/],
175             }
176              
177             The fields which hold empty strings or undefined values will not be inserted,
178             but just ignored.
179              
180             =cut
181              
182             sub quick_insert {
183 0     0 1   my ($self, $dn, $origref, %opts) = @_;
184 0           my ($mesg);
185              
186             # escape DN
187 0           $dn = $self->dn_escape($dn);
188              
189             # shallow copy of the ref
190 0           my $ref = {};
191             # sanitize the hash, LDAP *hates* empty strings
192 0           while (my ($k, $value) = each %$origref) {
193             # ignore undefined values
194 0 0         next unless defined $value;
195             # ignore empty strings
196 0 0 0       next if ((ref($value) eq '') and ($value eq ''));
197 0           $ref->{$k} = $value;
198             }
199              
200 0           Dancer::Logger::debug("LDAP insert, dn: ", $dn, "; data: ", $ref);
201            
202 0           $mesg = $self->add($dn, attr => [%$ref]);
203              
204 0 0         if ($mesg->code) {
205 0           return $self->_failure('insert', $mesg, $opts{errors});
206             }
207              
208 0           return $dn;
209             }
210              
211             =head2 quick_compare $type $a $b $pos
212              
213             =cut
214              
215             sub quick_compare {
216 0     0 1   my ($type, $a, $b, $pos) = @_;
217              
218 0 0         if ($type eq 'dn') {
219             # explode both distinguished names
220 0           my ($dn_a, $dn_b, $href_a, $href_b, $cmp);
221              
222 0           $dn_a = ldap_explode_dn($dn_a);
223 0           $dn_b = ldap_explode_dn($dn_b);
224              
225 0 0         if (@$dn_a > @$dn_b) {
    0          
226 0           return 1;
227             }
228             elsif (@$dn_a < @$dn_b) {
229 0           return -1;
230             }
231              
232             # check entries, starting from $pos
233 0   0       $pos ||= 0;
234              
235 0           for (my $i = $pos; $i < @$dn_a; $i++) {
236 0           $href_a = $dn_a->[$i];
237 0           $href_b = $dn_b->[$i];
238              
239 0           for my $k (keys %$href_a) {
240 0 0         unless (exists($href_b->{$k})) {
241 0           return 1;
242             }
243            
244 0 0         if ($cmp = $href_a->{$k} cmp $href_b->{$k}) {
245 0           return $cmp;
246             }
247              
248 0           delete $href_b->{$k};
249             }
250              
251 0 0         if (keys %$href_b) {
252 0           return -1;
253             }
254             }
255              
256 0           return 0;
257             }
258             }
259              
260             =head2 quick_update
261              
262             Modifies LDAP entry with distinguished name $dn by replacing the
263             values from $replace. If the value is the empty string, delete the
264             attribute.
265              
266             Returns DN in case of success.
267              
268             ldap->quick_update('uid=racke@linuxia.de,dc=linuxia,dc=de', {l => 'Vienna'});
269              
270             =cut
271              
272             sub quick_update {
273 0     0 1   my ($self, $dn, $spec_ref) = @_;
274 0           my ($mesg);
275              
276             # escape DN
277 0           $dn = $self->dn_escape($dn);
278              
279             # do a shallow copy of the hashref
280 0           my $spec_copy = { %$spec_ref };
281 0 0 0       if ($spec_copy and (ref($spec_copy) eq 'HASH')) {
282            
283             # check if there are empty values passed
284 0           while (my ($k, $v) = each %$spec_copy) {
285 0 0 0       if ((ref($v) eq '') and ($v eq '')) {
286             # in case replace them with an empty array ref to delete them
287 0           $spec_copy->{$k} = [];
288 0           Dancer::Logger::debug("$k is empty, replaced with []");
289             }
290             }
291             }
292              
293 0           Dancer::Logger::debug("LDAP update, dn: ", $dn, "; data: ", $spec_copy);
294              
295 0           $mesg = $self->modify(dn => $dn, replace => $spec_copy);
296              
297 0 0         if ($mesg->code) {
298 0           die "LDAP update failed (" . $mesg->code . ") with " . $mesg->error;
299             }
300            
301 0           return $dn;
302             }
303              
304             =head2 quick_delete
305              
306             Deletes entry given by distinguished name $dn.
307              
308             ldap->quick_delete('uid=racke@linuxia.de,dc=linuxia,dc=de');
309              
310             =cut
311              
312             sub quick_delete {
313 0     0 1   my ($self, $dn) = @_;
314 0           my ($ldret);
315              
316             # escape DN
317 0           $dn = $self->dn_escape($dn);
318              
319 0           Dancer::Logger::debug("LDAP delete: ", $dn);
320            
321 0           $ldret = $self->delete(dn => $dn);
322            
323 0 0         if ($ldret->code) {
324 0           die "LDAP delete failed (" . $ldret->code . ") with " . $ldret->error;
325             }
326              
327 0           return 1;
328             }
329              
330             =head2 rename
331              
332             Change distinguished name (DN) of a LDAP record from $old_dn to $new_dn.
333              
334             =cut
335              
336             sub rename {
337 0     0 1   my ($self, $old_dn, $new_dn) = @_;
338 0           my ($ldret, $old_ref, $new_ref, $rdn, $new_rdn, $superior, $ret,
339             $old_escaped);
340              
341 0           $old_ref = $self->dn_split($old_dn, hash => 1);
342 0           $new_ref = $self->dn_split($new_dn, hash => 1);
343              
344 0 0         if (@$new_ref == 1) {
345             # got already relative DN
346 0           $new_rdn = $new_dn;
347             }
348             else {
349             # relative DN is first
350 0           $rdn = shift @$new_ref;
351              
352             # check if it needs to move in the tree
353             # if ($self->compare($old_dn, $new_dn, 1)) {
354             # die "Different LDAP trees.";
355             # }
356              
357 0           $new_rdn = join('+', map {$_=$rdn->{$_}} keys %$rdn);
  0            
358             }
359              
360 0           $old_escaped = join(',', @$old_ref);
361              
362 0           Dancer::Logger::debug("LDAP rename from $old_escaped to $new_rdn.");
363              
364             # change distinguished name
365 0           $ldret = $self->moddn ($old_escaped, newrdn => $new_rdn);
366              
367 0 0         if ($ldret->code) {
368 0           return $self->_failure('rename', $ldret);
369             }
370              
371             # change attribute
372             # return $self->quick_update('');
373              
374 0           shift @$old_ref;
375 0           return $self->dn_unescape(join(',', $new_rdn, @$old_ref));
376             }
377              
378             =head2 base
379              
380             Returns base DN, optionally prepending relative DN from @rdn.
381              
382             ldap->base
383              
384             ldap->base('uid=racke@linuxia.de');
385              
386             =cut
387              
388             sub base {
389 0     0 1   my $self = shift;
390              
391 0 0         if (@_) {
392             # prepend path
393 0           return join(',', @_, $self->{dancer_settings}->{base});
394             }
395              
396 0           return $self->{dancer_settings}->{base};
397             }
398              
399             =head2 rebind
400              
401             Rebind with credentials from settings.
402              
403             =cut
404              
405             sub rebind {
406 0     0 1   my ($self) = @_;
407 0           my ($ldret);
408              
409 0           Dancer::Logger::debug("LDAP rebind to $self->{dancer_settings}->{bind}.");
410            
411 0           $ldret = $self->bind($self->{dancer_settings}->{bind},
412             password => $self->{dancer_settings}->{password});
413              
414 0 0         if ($ldret->code) {
415 0           Dancer::Logger::error('LDAP bind failed (' . $ldret->code . '): '
416             . $ldret->error);
417 0           return;
418             }
419              
420 0           return $self;
421             }
422              
423             =head2 dn_split $dn %options
424              
425             =cut
426              
427             sub dn_split {
428 0     0 1   my ($self, $dn, %options) = @_;
429 0           my (@frags, @dn_parts, @out, @tmp, $buf, $value);
430              
431             # break DN up with regular expresssions
432 0           @frags = reverse(split(/,/, $dn));
433              
434 0           $buf = '';
435              
436 0           for my $f (@frags) {
437 0           @tmp = split(/=/, $f);
438              
439 0 0         if ($buf) {
    0          
440 0           $value = "$tmp[1],$buf";
441             }
442             elsif (@tmp > 1) {
443 0           $value = $tmp[1];
444             }
445             else {
446 0           $value = $tmp[0];
447             }
448              
449 0 0         if (@tmp > 1) {
450 0 0         if ($options{raw}) {
451 0           unshift @dn_parts, "$tmp[0]=" . $value;
452             }
453             else {
454 0           unshift @dn_parts, "$tmp[0]=" . escape_dn_value($value);
455             }
456 0           $buf = '';
457             }
458             else {
459 0           $buf = $value;
460             }
461             }
462              
463 0 0         if ($options{hash}) {
464 0           return \@dn_parts;
465             }
466              
467 0           return join(',', @dn_parts);
468             }
469              
470             =head2 dn_join $rdn1 $rdn2 ...
471              
472             =cut
473              
474             sub dn_join {
475 0     0 1   my ($self, @rdn_list) = @_;
476 0           my (@out);
477              
478 0           for my $rdn (@rdn_list) {
479 0 0         if (ref($rdn) eq 'HASH') {
480 0           push (@out, join '+',
481 0           map {"$_=" . $rdn->{$_}} keys %$rdn);
482             }
483             else {
484 0           push (@out, $rdn);
485             }
486             }
487              
488 0           return join(',', @out);
489             }
490              
491             =head2 dn_escape
492              
493             Escapes values in DN $dn and returns the altered string.
494              
495             =cut
496              
497             sub dn_escape {
498 0     0 1   my ($self, $dn) = @_;
499              
500 0           return $self->dn_split($dn);
501             }
502              
503             =head2 dn_unescape
504              
505             Unescapes values in DN $dn and returns the altered string.
506              
507             =cut
508              
509             sub dn_unescape {
510 0     0 1   my ($self, $dn) = @_;
511 0           my ($dn_ref);
512              
513 0           $dn_ref = ldap_explode_dn($dn);
514              
515 0           return $self->dn_join(@$dn_ref);
516             }
517              
518             =head2 dn_value $dn $pos $attribute
519              
520             Returns DN attribute value from $dn at position $pos,
521             matching attribute name $attribute.
522              
523             $pos and $attribute are optional.
524              
525             Returns undef in the following cases:
526              
527             * invalid DN
528             * $pos exceeds number of entries in the DN
529             * attribute name doesn't match $attribute
530              
531             Examples:
532              
533             ldap->dn_value('ou=Testing,dc=linuxia,dc=de');
534              
535             Testing
536              
537             ldap->dn_value('ou=Testing,dc=linuxia,dc=de', 1);
538              
539             linuxia
540              
541             =cut
542              
543             sub dn_value {
544 0     0 1   my ($self, $dn, $pos, $attribute) = @_;
545 0           my ($new_ref, $entry);
546              
547 0           $new_ref = ldap_explode_dn($dn);
548 0   0       $pos ||= 0;
549              
550 0 0         unless (defined $new_ref) {
551 0           return;
552             }
553              
554 0 0         if ($pos >= @$new_ref) {
555 0           return;
556             }
557              
558 0           $entry = $new_ref->[$pos];
559              
560 0 0         if (defined $attribute) {
561             # keys are by default uppercase
562 0           $attribute = uc($attribute);
563              
564 0 0         if (exists $entry->{$attribute}) {
565 0           return $entry->{$attribute};
566             }
567              
568 0           return;
569             }
570              
571 0           return $entry->{values(%$entry)->[0]};
572             }
573              
574             sub _failure {
575 0     0     my ($self, $op, $mesg, $options) = @_;
576              
577 0 0         if ($options) {
578 0 0         if (ref($options) eq 'HASH') {
579 0 0         if ($mesg->code == 68) {
580             # "Already exists"
581 0 0         if ($options->{exists}) {
582 0           return;
583             }
584             }
585             }
586             }
587              
588 0           my $errmsg = "LDAP $op failed (" . $mesg->code . ") with " . $mesg->error;
589              
590 0 0         if ($mesg->dn) {
591 0           $errmsg .= ' (DN: ' . $mesg->dn . ')';
592             }
593              
594 0           die $errmsg;
595             }
596              
597             # build conditions for LDAP searches
598              
599             sub _build_conditions {
600 0     0     my ($self, $spec_ref) = @_;
601 0           my ($key, $value, $safe_value, @conds, @sub_conds);
602              
603 0           while (($key, $value) = each(%$spec_ref)) {
604 0 0         if ($key eq '-or') {
    0          
605 0           push @conds, '(|' . join('', $self->_build_conditions($value)) . ')';
606             } elsif (ref($value) eq 'ARRAY') {
607             # Operator requested
608 0 0 0       if ($value->[0] eq 'exists') {
    0 0        
    0 0        
    0 0        
      0        
609 0 0         if ($value->[1]) {
610             # attribute present
611 0           push (@conds, "($key=*)");
612             }
613             else {
614             # attribute missing
615 0           push (@conds, "(!($key=*))");
616             }
617             }
618             elsif ($value->[0] eq '!' || $value->[0] eq 'not') {
619 0           push (@conds, "(!($key=$value->[1]))");
620             }
621             elsif ($value->[0] eq 'substr'
622             || $value->[0] eq 'substring') {
623 0           push (@conds, "($key=*" . escape_filter_value($value->[1]) . "*)");
624             }
625             elsif ($value->[0] eq '<'
626             || $value->[0] eq '<='
627             || $value->[0] eq '>'
628             || $value->[0] eq '>=') {
629 0           push (@conds, "($key$value->[0]" . escape_filter_value($value->[1]) . ')');
630             }
631             else {
632 0           Dancer::Logger::debug("Invalid operator for $key: ", $value);
633 0           die "Invalid operator $value->[0].";
634             }
635             }
636             else {
637             # escape filter value first
638 0           $safe_value = escape_filter_value($value);
639 0           push (@conds, "($key=$safe_value)");
640             }
641             }
642              
643 0           return @conds;
644             }
645              
646             # fix UTF-8 encoding
647             sub _utf8_decode {
648 0     0     my ($self, $string) = @_;
649              
650 0 0         unless(Encode::is_utf8($string)){
651 0           $string = Encode::decode('utf-8', $string);
652             }
653              
654 0           return $string;
655             }
656              
657             =head1 DN
658              
659             Our methods return and expect unescaped DN's.
660              
661             =head1 AUTHOR
662              
663             Stefan Hornburg (Racke),
664              
665             =head1 ACKNOWLEDGEMENTS
666              
667             See L
668              
669             =head1 LICENSE AND COPYRIGHT
670              
671             Copyright 2010-2013 Stefan Hornburg (Racke) .
672              
673             This program is free software; you can redistribute it and/or modify it
674             under the terms of either: the GNU General Public License as published
675             by the Free Software Foundation; or the Artistic License.
676              
677             See http://dev.perl.org/licenses/ for more information.
678              
679             =head1 SEE ALSO
680              
681             L
682              
683             L
684              
685             L
686              
687             =cut
688              
689             1;
690