]> git.proxmox.com Git - pve-common.git/blob - src/PVE/LDAP.pm
bump version to 8.1.2
[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 );
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;