add LDAP Wrapper code
[pve-common.git] / src / PVE / LDAP.pm
1 package PVE::LDAP;
2
3 use strict;
4 use warnings;
5
6 use Net::IP;
7 use Net::LDAP;
8 use Net::LDAP::Control::Paged;
9 use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED);
10
11 sub ldap_connect {
12     my ($servers, $scheme, $port, $opts) = @_;
13
14     my $start_tls = 0;
15
16     if ($scheme eq 'ldap+starttls') {
17         $scheme = 'ldap';
18         $start_tls = 1;
19     }
20
21     my %ldap_opts = (
22         scheme => $scheme,
23         port => $port,
24         timeout => 10,
25         onerror => 'die',
26     );
27
28     my $hosts = [];
29     for my $host (@$servers) {
30         if (Net::IP::ip_is_ipv6($host)) {
31             push @$hosts, "[$host]";
32         } else {
33             push @$hosts, $host;
34         }
35     }
36
37     for my $opt (qw(clientcert clientkey capath cafile sslversion verify)) {
38         $ldap_opts{$opt} = $opts->{$opt} if $opts->{$opt};
39     }
40
41     my $ldap = Net::LDAP->new($hosts, %ldap_opts) || die $@;
42
43     if ($start_tls) {
44         $ldap->start_tls(%$opts);
45     }
46
47     return $ldap;
48 }
49
50 sub ldap_bind {
51     my ($ldap, $dn, $pw) = @_;
52
53     my $res;
54     if (defined($dn) && defined($pw)) {
55         $res = $ldap->bind($dn, password => $pw);
56     } else { # anonymous bind
57         $res = $ldap->bind();
58     }
59
60     my $code = $res->code;
61     my $err = $res->error;
62
63     die "ldap bind failed: $err\n" if $code;
64 }
65
66 sub get_user_dn {
67     my ($ldap, $name, $attr, $base_dn) = @_;
68
69     # search for dn
70     my $result = $ldap->search(
71         base    => $base_dn // "",
72         scope   => "sub",
73         filter  => "$attr=$name",
74         attrs   => ['dn']
75     );
76     return undef if !$result->entries;
77     my @entries = $result->entries;
78     return $entries[0]->dn;
79 }
80
81 sub auth_user_dn {
82     my ($ldap, $dn, $pw, $noerr) = @_;
83     my $res = $ldap->bind($dn, password => $pw);
84
85     my $code = $res->code;
86     my $err = $res->error;
87
88     if ($code) {
89         return undef if $noerr;
90         die $err;
91     }
92
93     return 1;
94 }
95
96 sub query_users {
97     my ($ldap, $filter, $attributes, $base_dn) = @_;
98
99     # build filter from given filter and attribute list
100     my $tmp = "(|";
101     foreach my $att (@$attributes) {
102         $tmp .= "($att=*)";
103     }
104     $tmp .= ")";
105
106     if ($filter) {
107         $filter = "($filter)" if $filter !~ m/^\(.*\)$/;
108         $filter = "(&${filter}${tmp})"
109     } else {
110         $filter = $tmp;
111     }
112
113     my $page = Net::LDAP::Control::Paged->new(size => 900);
114
115     my @args = (
116         base     => $base_dn // "",
117         scope    => "subtree",
118         filter   => $filter,
119         control  => [ $page ],
120         attrs    => [ @$attributes, 'memberOf'],
121     );
122
123     my $cookie;
124     my $err;
125     my $users = [];
126
127     while(1) {
128
129         my $mesg = $ldap->search(@args);
130
131         # stop on error
132         if ($mesg->code)  {
133             $err = "ldap user search error: " . $mesg->error;
134             last;
135         }
136
137         #foreach my $entry ($mesg->entries) { $entry->dump; }
138         foreach my $entry ($mesg->entries) {
139             my $user = {
140                 dn => $entry->dn,
141                 attributes => {},
142                 groups => [$entry->get_value('memberOf')],
143             };
144
145             foreach my $attr (@$attributes) {
146                 my $vals = [$entry->get_value($attr)];
147                 if (scalar(@$vals)) {
148                     $user->{attributes}->{$attr} = $vals;
149                 }
150             }
151
152             push @$users, $user;
153         }
154
155         # Get cookie from paged control
156         my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
157         $cookie = $resp->cookie;
158
159         last if (!defined($cookie) || !length($cookie));
160
161         # Set cookie in paged control
162         $page->cookie($cookie);
163     }
164
165     if (defined($cookie) && length($cookie)) {
166         # We had an abnormal exit, so let the server know we do not want any more
167         $page->cookie($cookie);
168         $page->size(0);
169         $ldap->search(@args);
170         $err = "LDAP user query unsuccessful" if !$err;
171     }
172
173     die $err if $err;
174
175     return $users;
176 }
177
178 sub query_groups {
179     my ($ldap, $base_dn, $classes, $filter) = @_;
180
181     my $tmp = "(|";
182     for my $class (@$classes) {
183         $tmp .= "(objectclass=$class)";
184     }
185     $tmp .= ")";
186
187     if ($filter) {
188         $filter = "($filter)" if $filter !~ m/^\(.*\)$/;
189         $filter = "(&${filter}${tmp})"
190     } else {
191         $filter = $tmp;
192     }
193
194     my $page = Net::LDAP::Control::Paged->new(size => 100);
195
196     my @args = (
197         base     => $base_dn,
198         scope    => "subtree",
199         filter   => $filter,
200         control  => [ $page ],
201         attrs    => [ 'member', 'uniqueMember' ],
202     );
203
204     my $cookie;
205     my $err;
206     my $groups = [];
207
208     while(1) {
209
210         my $mesg = $ldap->search(@args);
211
212         # stop on error
213         if ($mesg->code)  {
214             $err = "ldap group search error: " . $mesg->error;
215             last;
216         }
217
218         foreach my $entry ( $mesg->entries ) {
219             my $group = {
220                 dn => $entry->dn,
221                 members => []
222             };
223             my $members = [$entry->get_value('member')];
224             if (!scalar(@$members)) {
225                 $members = [$entry->get_value('uniqueMember')];
226             }
227             $group->{members} = $members;
228             push @$groups, $group;
229         }
230
231         # Get cookie from paged control
232         my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
233         $cookie = $resp->cookie;
234
235         last if (!defined($cookie) || !length($cookie));
236
237         # Set cookie in paged control
238         $page->cookie($cookie);
239     }
240
241     if ($cookie) {
242         # We had an abnormal exit, so let the server know we do not want any more
243         $page->cookie($cookie);
244         $page->size(0);
245         $ldap->search(@args);
246         $err = "LDAP group query unsuccessful" if !$err;
247     }
248
249     die $err if $err;
250
251     return $groups;
252 }
253
254 1;