]>
Commit | Line | Data |
---|---|---|
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 | ); | |
26 | ||
27 | my $hosts = []; | |
28 | for my $host (@$servers) { | |
29 | if (Net::IP::ip_is_ipv6($host)) { | |
30 | push @$hosts, "[$host]"; | |
31 | } else { | |
32 | push @$hosts, $host; | |
33 | } | |
34 | } | |
35 | ||
36 | for my $opt (qw(clientcert clientkey capath cafile sslversion verify)) { | |
37 | $ldap_opts{$opt} = $opts->{$opt} if $opts->{$opt}; | |
38 | } | |
39 | ||
40 | my $ldap = Net::LDAP->new($hosts, %ldap_opts) || die "$@\n"; | |
41 | ||
42 | if ($start_tls) { | |
43 | my $res = $ldap->start_tls(%$opts); | |
44 | die $res->error . "\n" if $res->code; | |
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 | die $result->error . "\n" if $result->code; | |
77 | return undef if !$result->entries; | |
78 | my @entries = $result->entries; | |
79 | return $entries[0]->dn; | |
80 | } | |
81 | ||
82 | sub auth_user_dn { | |
83 | my ($ldap, $dn, $pw, $noerr) = @_; | |
84 | ||
85 | if (!$dn) { | |
86 | return undef if $noerr; | |
87 | die "user dn is empty\n"; | |
88 | } | |
89 | ||
90 | my $res = $ldap->bind($dn, password => $pw); | |
91 | ||
92 | my $code = $res->code; | |
93 | my $err = $res->error; | |
94 | ||
95 | if ($code) { | |
96 | return undef if $noerr; | |
97 | die "$err\n"; | |
98 | } | |
99 | ||
100 | return 1; | |
101 | } | |
102 | ||
103 | sub query_users { | |
104 | my ($ldap, $filter, $attributes, $base_dn, $classes) = @_; | |
105 | ||
106 | # build filter from given filter and attribute list | |
107 | my $tmp = "(|"; | |
108 | foreach my $att (@$attributes) { | |
109 | $tmp .= "($att=*)"; | |
110 | } | |
111 | $tmp .= ")"; | |
112 | ||
113 | if ($classes) { | |
114 | $tmp = "(&$tmp(|"; | |
115 | for my $class (@$classes) { | |
116 | $tmp .= "(objectclass=$class)"; | |
117 | } | |
118 | $tmp .= "))"; | |
119 | } | |
120 | ||
121 | if ($filter) { | |
122 | $filter = "($filter)" if $filter !~ m/^\(.*\)$/; | |
123 | $filter = "(&${filter}${tmp})" | |
124 | } else { | |
125 | $filter = $tmp; | |
126 | } | |
127 | ||
128 | my $page = Net::LDAP::Control::Paged->new(size => 900); | |
129 | ||
130 | my @args = ( | |
131 | base => $base_dn // "", | |
132 | scope => "subtree", | |
133 | filter => $filter, | |
134 | control => [ $page ], | |
135 | attrs => [ @$attributes, 'memberOf'], | |
136 | ); | |
137 | ||
138 | my $cookie; | |
139 | my $err; | |
140 | my $users = []; | |
141 | ||
142 | while(1) { | |
143 | ||
144 | my $mesg = $ldap->search(@args); | |
145 | ||
146 | # stop on error | |
147 | if ($mesg->code) { | |
148 | $err = "ldap user search error: " . $mesg->error; | |
149 | last; | |
150 | } | |
151 | ||
152 | #foreach my $entry ($mesg->entries) { $entry->dump; } | |
153 | foreach my $entry ($mesg->entries) { | |
154 | my $user = { | |
155 | dn => $entry->dn, | |
156 | attributes => {}, | |
157 | groups => [$entry->get_value('memberOf')], | |
158 | }; | |
159 | ||
160 | foreach my $attr (@$attributes) { | |
161 | my $vals = [$entry->get_value($attr)]; | |
162 | if (scalar(@$vals)) { | |
163 | $user->{attributes}->{$attr} = $vals; | |
164 | } | |
165 | } | |
166 | ||
167 | push @$users, $user; | |
168 | } | |
169 | ||
170 | # Get cookie from paged control | |
171 | my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last; | |
172 | $cookie = $resp->cookie; | |
173 | ||
174 | last if (!defined($cookie) || !length($cookie)); | |
175 | ||
176 | # Set cookie in paged control | |
177 | $page->cookie($cookie); | |
178 | } | |
179 | ||
180 | if (defined($cookie) && length($cookie)) { | |
181 | # We had an abnormal exit, so let the server know we do not want any more | |
182 | $page->cookie($cookie); | |
183 | $page->size(0); | |
184 | $ldap->search(@args); | |
185 | $err = "LDAP user query unsuccessful" if !$err; | |
186 | } | |
187 | ||
188 | die "$err\n" if $err; | |
189 | ||
190 | return $users; | |
191 | } | |
192 | ||
193 | sub query_groups { | |
194 | my ($ldap, $base_dn, $classes, $filter, $group_name_attr) = @_; | |
195 | ||
196 | my $tmp = "(|"; | |
197 | for my $class (@$classes) { | |
198 | $tmp .= "(objectclass=$class)"; | |
199 | } | |
200 | $tmp .= ")"; | |
201 | ||
202 | if ($filter) { | |
203 | $filter = "($filter)" if $filter !~ m/^\(.*\)$/; | |
204 | $filter = "(&${filter}${tmp})" | |
205 | } else { | |
206 | $filter = $tmp; | |
207 | } | |
208 | ||
209 | my $page = Net::LDAP::Control::Paged->new(size => 100); | |
210 | ||
211 | my $attrs = [ 'member', 'uniqueMember' ]; | |
212 | push @$attrs, $group_name_attr if $group_name_attr; | |
213 | my @args = ( | |
214 | base => $base_dn, | |
215 | scope => "subtree", | |
216 | filter => $filter, | |
217 | control => [ $page ], | |
218 | attrs => $attrs, | |
219 | ); | |
220 | ||
221 | my $cookie; | |
222 | my $err; | |
223 | my $groups = []; | |
224 | ||
225 | while(1) { | |
226 | ||
227 | my $mesg = $ldap->search(@args); | |
228 | ||
229 | # stop on error | |
230 | if ($mesg->code) { | |
231 | $err = "ldap group search error: " . $mesg->error; | |
232 | last; | |
233 | } | |
234 | ||
235 | foreach my $entry ( $mesg->entries ) { | |
236 | my $group = { | |
237 | dn => $entry->dn, | |
238 | members => [] | |
239 | }; | |
240 | my $members = [$entry->get_value('member')]; | |
241 | if (!scalar(@$members)) { | |
242 | $members = [$entry->get_value('uniqueMember')]; | |
243 | } | |
244 | $group->{members} = $members; | |
245 | if ($group_name_attr && (my $name = $entry->get_value($group_name_attr))) { | |
246 | $group->{name} = $name; | |
247 | } | |
248 | push @$groups, $group; | |
249 | } | |
250 | ||
251 | # Get cookie from paged control | |
252 | my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last; | |
253 | $cookie = $resp->cookie; | |
254 | ||
255 | last if (!defined($cookie) || !length($cookie)); | |
256 | ||
257 | # Set cookie in paged control | |
258 | $page->cookie($cookie); | |
259 | } | |
260 | ||
261 | if ($cookie) { | |
262 | # We had an abnormal exit, so let the server know we do not want any more | |
263 | $page->cookie($cookie); | |
264 | $page->size(0); | |
265 | $ldap->search(@args); | |
266 | $err = "LDAP group query unsuccessful" if !$err; | |
267 | } | |
268 | ||
269 | die "$err\n" if $err; | |
270 | ||
271 | return $groups; | |
272 | } | |
273 | ||
274 | 1; |