]> git.proxmox.com Git - pmg-api.git/blob - PMG/LDAPCache.pm
add groupclass as parameter in LDAPConfig
[pmg-api.git] / PMG / LDAPCache.pm
1 package PMG::LDAPCache;
2
3 use strict;
4 use warnings;
5 use File::Path;
6 use LockFile::Simple;
7 use Data::Dumper;
8 use Net::LDAP;
9 use Net::LDAP::Control::Paged;
10 use Net::LDAP::Constant qw (LDAP_CONTROL_PAGED);
11 use DB_File;
12
13 use PVE::SafeSyslog;
14 use PVE::Tools qw(split_list);
15
16 use PMG::Utils;
17 use PMG::LDAPConfig;
18
19 $DB_HASH->{'cachesize'} = 10000;
20 $DB_RECNO->{'cachesize'} = 10000;
21 $DB_BTREE->{'cachesize'} = 10000;
22 $DB_BTREE->{'flags'} = R_DUP ;
23
24 my $cachedir = '/var/lib/pmg';
25
26 my $last_atime = {};
27 my $ldapcache = {};
28
29 # DB Description
30 #
31 # users (hash): UID -> pmail, account, DN
32 # dnames (hash): DN -> UID
33 # accounts (hash): account -> UID
34 # mail (hash): mail -> UID
35 # groups (hash): group -> GID
36 # memberof (btree): UID -> GID
37 #
38 my @dbs = ('users', 'dnames', 'groups', 'mails', 'accounts', 'memberof');
39
40 sub new {
41 my ($self, %args) = @_;
42
43 my $type = ref($self) || $self;
44
45 die "undefined ldap id" if !$args{id};
46
47 my $id = $args{id};
48
49 if ($ldapcache->{$id}) {
50 $self = $ldapcache->{$id};
51 } else {
52 $ldapcache->{$id} = $self = bless {}, $type;
53 $self->{id} = $id;
54 }
55
56 my $config_properties = PMG::LDAPConfig::properties();
57
58 # set defaults for the fields that have one
59 foreach my $property (keys %$config_properties) {
60 my $d = $config_properties->{$property};
61 next if !defined($d->{default});
62 $self->{$property} = $args{$property} || $d->{default};
63 }
64
65 # split list returns an array not a reference
66 $self->{accountattr} = [split_list($self->{accountattr})];
67 $self->{mailattr} = [split_list($self->{mailattr})];
68 $self->{groupclass} = [split_list($self->{groupclass})];
69
70 $self->{server1} = $args{server1};
71 $self->{server2} = $args{server2};
72 $self->{binddn} = $args{binddn};
73 $self->{bindpw} = $args{bindpw};
74 $self->{basedn} = $args{basedn};
75 $self->{port} = $args{port};
76 $self->{groupbasedn} = $args{groupbasedn};
77 $self->{filter} = $args{filter};
78
79 if ($args{syncmode} == 1) {
80 # read local data only
81 $self->{errors} = '';
82 $self->loadcache();
83 return $self;
84 }
85
86 return $self if !($args{server1});
87
88 if ($args{syncmode} == 2) {
89 # force sync
90 $self->loaddata(1);
91 } else {
92 $self->loaddata();
93 }
94
95 return $self;
96 }
97
98 sub lockdir {
99 my ($id) = @_;
100
101 my $dir = "$cachedir/ldapdb_$id";
102 my $scheme = LockFile::Simple->make(
103 -warn => 0, -stale => 1, -autoclean => 1);
104 my $lock = $scheme->lock($dir);
105
106 return $lock;
107 }
108
109 sub delete {
110 my ($class, $id) = @_;
111
112 if (my $lock = lockdir($id)) {
113 delete $ldapcache->{$id};
114 delete $last_atime->{$id};
115 my $dir = "$cachedir/ldapdb_$id";
116 rmtree $dir;
117 $lock->release;
118 } else {
119 syslog('err' , "can't lock ldap database '$id'");
120 }
121 }
122
123 sub update {
124 my ($self, $syncmode) = @_;
125
126 if ($syncmode == 1) {
127 # read local data only
128 $self->{errors} = '';
129 $self->loadcache();
130 } elsif ($syncmode == 2) {
131 # force sync
132 $self->loaddata(1);
133 } else {
134 $self->loaddata();
135 }
136 }
137
138 sub queryusers {
139 my ($self, $ldap) = @_;
140
141 my $filter = '(|';
142 foreach my $attr (@{$self->{mailattr}}) {
143 $filter .= "($attr=*)";
144 }
145 $filter .= ')';
146
147 if ($self->{filter}) {
148 my $tmp = $self->{filter};
149 $tmp = "($tmp)" if $tmp !~ m/^\(.*\)$/;
150
151 $filter = "(&${filter}${tmp})";
152 }
153
154 my $page = Net::LDAP::Control::Paged->new(size => 900);
155
156 my @args = (
157 base => $self->{basedn},
158 scope => "subtree",
159 filter => $filter,
160 control => [ $page ],
161 attrs => [ @{$self->{mailattr}}, @{$self->{accountattr}}, 'memberOf' ]
162 );
163
164 my $cookie;
165
166 while(1) {
167
168 my $mesg = $ldap->search(@args);
169
170 # stop on error
171 if ($mesg->code) {
172 my $err = "ldap user search error: " . $mesg->error;
173 $self->{errors} .= "$err\n";
174 syslog('err', $err);
175 last;
176 }
177
178 #foreach my $entry ($mesg->entries) { $entry->dump; }
179 foreach my $entry ($mesg->entries) {
180 my $dn = $entry->dn;
181
182 my $umails = {};
183 my $pmail;
184
185 foreach my $attr (@{$self->{mailattr}}) {
186 foreach my $mail ($entry->get_value($attr)) {
187 $mail = lc($mail);
188 # Test if the Line starts with one of the following lines:
189 # proxyAddresses: [smtp|SMTP]:
190 # and also discard this starting string, so that $mail is only the
191 # address without any other characters...
192
193 $mail =~ s/^(smtp|SMTP)[\:\$]//gs;
194
195 if ($mail !~ m/[\{\}\\\/]/ && $mail =~ m/^\S+\@\S+$/) {
196 $umails->{$mail} = 1;
197 $pmail = $mail if !$pmail;
198 }
199 }
200 }
201 my $addresses = [ keys %$umails ];
202
203 next if !$pmail; # account has no email addresses
204
205 my $cuid;
206 $self->{dbstat}->{dnames}->{dbh}->get($dn, $cuid);
207 if (!$cuid) {
208 $cuid = ++$self->{dbstat}->{dnames}->{idcount};
209 $self->{dbstat}->{dnames}->{dbh}->put($dn, $cuid);
210 }
211
212 foreach my $attr (@{$self->{accountattr}}) {
213 my $account = $entry->get_value($attr);
214 if ($account && ($account =~ m/^\S+$/s)) {
215 $account = lc($account);
216 $self->{dbstat}->{accounts}->{dbh}->put($account, $cuid);
217 my $data = pack('n/a* n/a* n/a*', $pmail, $account, $dn);
218 $self->{dbstat}->{users}->{dbh}->put($cuid, $data);
219 }
220 }
221
222 foreach my $mail (@$addresses) {
223 $self->{dbstat}->{mails}->{dbh}->put($mail, $cuid);
224 }
225
226 if (!$self->{groupbasedn}) {
227 my @groups = $entry->get_value('memberOf');
228 foreach my $group (@groups) {
229 my $cgid;
230 $self->{dbstat}->{groups}->{dbh}->get($group, $cgid);
231 if (!$cgid) {
232 $cgid = ++$self->{dbstat}->{groups}->{idcount};
233 $self->{dbstat}->{groups}->{dbh}->put($group, $cgid);
234 }
235 $self->{dbstat}->{memberof}->{dbh}->put($cuid, $cgid);
236 }
237 }
238 }
239
240 # Get cookie from paged control
241 my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
242 $cookie = $resp->cookie or last;
243
244 # Set cookie in paged control
245 $page->cookie($cookie);
246 }
247
248
249 if ($cookie) {
250 # We had an abnormal exit, so let the server know we do not want any more
251 $page->cookie($cookie);
252 $page->size(0);
253 $ldap->search(@args);
254 my $err = "LDAP user query unsuccessful";
255 $self->{errors} .= "$err\n";
256 syslog('err', $err);
257 }
258 }
259
260 sub querygroups {
261 my ($self, $ldap) = @_;
262
263 return undef if !$self->{groupbasedn};
264
265 my $filter = "(|";
266
267 for my $class (@{$self->{groupclass}}) {
268 $filter .= "(objectclass=$class)";
269 }
270
271 $filter .= ")";
272
273 my $page = Net::LDAP::Control::Paged->new(size => 100);
274
275 my @args = ( base => $self->{groupbasedn},
276 scope => "subtree",
277 filter => $filter,
278 control => [ $page ],
279 attrs => [ 'member', 'uniqueMember' ],
280 );
281
282 my $cookie;
283 while(1) {
284
285 my $mesg = $ldap->search(@args);
286
287 # stop on error
288 if ($mesg->code) {
289 my $err = "ldap group search error: " . $mesg->error;
290 $self->{errors} .= "$err\n";
291 syslog('err', $err);
292 last;
293 }
294
295 foreach my $entry ( $mesg->entries ) {
296 my $group = $entry->dn;
297 my @members = $entry->get_value('member');
298 if (!scalar(@members)) {
299 @members = $entry->get_value('uniqueMember');
300 }
301 my $cgid;
302 $self->{dbstat}->{groups}->{dbh}->get($group, $cgid);
303 if (!$cgid) {
304 $cgid = ++$self->{dbstat}->{groups}->{idcount};
305 $self->{dbstat}->{groups}->{dbh}->put($group, $cgid);
306 }
307
308 foreach my $m (@members) {
309
310 my $cuid;
311 $self->{dbstat}->{dnames}->{dbh}->get($m, $cuid);
312 if (!$cuid) {
313 $cuid = ++$self->{dbstat}->{dnames}->{idcount};
314 $self->{dbstat}->{dnames}->{dbh}->put($m, $cuid);
315 }
316
317 $self->{dbstat}->{memberof}->{dbh}->put($cuid, $cgid);
318 }
319 }
320
321 # Get cookie from paged control
322 my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
323 $cookie = $resp->cookie or last;
324
325 # Set cookie in paged control
326 $page->cookie($cookie);
327 }
328
329 if ($cookie) {
330 # We had an abnormal exit, so let the server know we do not want any more
331 $page->cookie($cookie);
332 $page->size(0);
333 $ldap->search(@args);
334 my $err = "LDAP group query unsuccessful";
335 $self->{errors} .= "$err\n";
336 syslog('err', $err);
337 }
338 }
339
340 sub ldap_connect {
341 my ($self) = @_;
342
343 my $hosts = [ $self->{server1} ];
344
345 push @$hosts, $self->{server2} if $self->{server2};
346
347 my $opts = { timeout => 10, onerror => 'die' };
348
349 $opts->{port} = $self->{port} if $self->{port};
350 $opts->{schema} = $self->{mode};
351
352 return Net::LDAP->new($hosts, %$opts);
353 }
354
355 sub ldap_connect_and_bind {
356 my ($self) = @_;
357
358 my $ldap = $self->ldap_connect() ||
359 die "Can't bind to ldap server '$self->{id}': $!\n";
360
361 my $mesg;
362
363 if ($self->{binddn}) {
364 $mesg = $ldap->bind($self->{binddn}, password => $self->{bindpw});
365 } else {
366 $mesg = $ldap->bind(); # anonymous bind
367 }
368
369 die "ldap bind failed: " . $mesg->error . "\n" if $mesg->code;
370
371 if (!$self->{basedn}) {
372 my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]);
373 $self->{basedn} = $root->get_value('defaultNamingContext');
374 }
375
376 return $ldap;
377 }
378
379 sub sync_database {
380 my ($self) = @_;
381
382 my $dir = "ldapdb_" . $self->{id};
383 mkdir "$cachedir/$dir";
384
385 # open ldap connection
386
387 my $ldap;
388
389 eval { $ldap = $self->ldap_connect_and_bind(); };
390 if (my $err = $@) {
391 $self->{errors} .= "$err\n";
392 syslog('err', $err);
393 return;
394 }
395
396 # open temporary database files
397
398 my $olddbh = {};
399
400 foreach my $db (@dbs) {
401 $self->{dbstat}->{$db}->{tmpfilename} = "$cachedir/$dir/${db}_tmp$$.db";
402 $olddbh->{$db} = $self->{dbstat}->{$db}->{dbh};
403 }
404
405 my $error_cleanup = sub {
406 # close and delete all files
407 foreach my $db (@dbs) {
408 undef $self->{dbstat}->{$db}->{dbh};
409 unlink $self->{dbstat}->{$db}->{tmpfilename};
410 $self->{dbstat}->{$db}->{dbh} = $olddbh->{$db};
411 }
412 };
413
414 eval {
415 foreach my $db (@dbs) {
416 my $filename = $self->{dbstat}->{$db}->{tmpfilename};
417 $self->{dbstat}->{$db}->{idcount} = 0;
418 unlink $filename;
419
420 if ($db eq 'memberof') {
421 $self->{dbstat}->{$db}->{dbh} =
422 tie (my %h, 'DB_File', $filename,
423 O_CREAT|O_RDWR, 0666, $DB_BTREE);
424 } else {
425 $self->{dbstat}->{$db}->{dbh} =
426 tie (my %h, 'DB_File', $filename,
427 O_CREAT|O_RDWR, 0666, $DB_HASH);
428 }
429
430 die "unable to open database file '$filename': $!\n"
431 if !$self->{dbstat}->{$db}->{dbh};
432 }
433 };
434 if (my $err = $@) {
435 $error_cleanup->();
436 $self->{errors} .= $err;
437 syslog('err', $err);
438 return;
439 }
440
441 $self->querygroups ($ldap) if $self->{groupbasedn};
442
443 $self->queryusers($ldap) if !$self->{errors};
444
445 $ldap->unbind;
446
447 if ($self->{errors}) {
448 $error_cleanup->();
449 return;
450 }
451
452 my $lock = lockdir($self->{id});
453
454 if (!$lock) {
455 my $err = "unable to get database lock for ldap database '$self->{id}'";
456 $self->{errors} .= "$err\n";
457 syslog('err', $err);
458 $error_cleanup->();
459 return;
460 }
461
462 foreach my $db (@dbs) {
463 my $filename = $self->{dbstat}->{$db}->{filename} =
464 "$cachedir/$dir/${db}.db";
465 $self->{dbstat}->{$db}->{dbh}->sync(); # flush everything
466 rename $self->{dbstat}->{$db}->{tmpfilename}, $filename;
467 }
468
469 $lock->release;
470
471 $last_atime->{$self->{id}} = time();
472
473 $self->{gcount} = $self->{dbstat}->{groups}->{idcount};
474 $self->{ucount} = __count_entries($self->{dbstat}->{accounts}->{dbh});
475 $self->{mcount} = __count_entries($self->{dbstat}->{mails}->{dbh});
476 }
477
478 sub __count_entries {
479 my ($dbh) = @_;
480
481 return 0 if !$dbh;
482
483 my $key = 0 ;
484 my $value = "" ;
485 my $count = 0;
486 my $status = $dbh->seq($key, $value, R_FIRST());
487
488 while ($status == 0) {
489 $count++;
490 $status = $dbh->seq($key, $value, R_NEXT());
491 }
492
493 return $count;
494 }
495
496 sub loadcache {
497 my ($self, $try) = @_;
498
499 my $dir = "ldapdb_" . $self->{id};
500 mkdir "$cachedir/$dir";
501
502 my $filename = "$cachedir/$dir/mails.db";
503
504 return if $last_atime->{$self->{id}} &&
505 PMG::Utils::file_older_than ($filename, $last_atime->{$self->{id}});
506
507 eval {
508 foreach my $db (@dbs) {
509 my $filename = $self->{dbstat}->{$db}->{filename} =
510 "$cachedir/$dir/${db}.db";
511 $self->{dbstat}->{$db}->{idcount} = 0;
512 if ($db eq 'memberof') {
513 $self->{dbstat}->{$db}->{dbh} =
514 tie (my %h, 'DB_File', $filename,
515 O_RDONLY, 0666, $DB_BTREE);
516 } else {
517 $self->{dbstat}->{$db}->{dbh} =
518 tie (my %h, 'DB_File', $filename,
519 O_RDONLY, 0666, $DB_HASH);
520 }
521
522 if (!$self->{dbstat}->{$db}->{dbh} && !$try) {
523 my $err = "ldap error - unable to open database file '$filename': $!";
524 $self->{errors} .= "$err\n";
525 syslog('err', $err) if !$self->{dbstat}->{$db}->{dbh};
526 }
527 }
528 };
529
530 $last_atime->{$self->{id}} = time();
531
532 $self->{gcount} = __count_entries($self->{dbstat}->{groups}->{dbh});
533 $self->{ucount} = __count_entries($self->{dbstat}->{accounts}->{dbh});
534 $self->{mcount} = __count_entries($self->{dbstat}->{mails}->{dbh});
535 }
536
537 sub loaddata {
538 my ($self, $force) = @_;
539
540 $self->{errors} = '';
541
542 if (!$force) {
543 # only sync if file is older than 1 hour
544
545 my $dir = "ldapdb_" . $self->{id};
546 mkdir "$cachedir/$dir";
547 my $filename = "$cachedir/$dir/mails.db";
548
549 if (-e $filename &&
550 !PMG::Utils::file_older_than($filename, time() - 3600)) {
551 $self->loadcache();
552 return;
553 }
554 }
555
556 $self->sync_database();
557
558 if ($self->{errors}) {
559 $self->loadcache(1);
560 }
561 }
562
563 sub list_groups {
564 my ($self) = @_;
565
566 my $res = [];
567
568 my $dbh = $self->{dbstat}->{groups}->{dbh};
569
570 return $res if !$dbh;
571
572 my $key = 0 ;
573 my $value = "" ;
574 my $status = $dbh->seq($key, $value, R_FIRST());
575 my $keys;
576
577 while ($status == 0) {
578 push @$res, {
579 dn => $key,
580 };
581 $status = $dbh->seq($key, $value, R_NEXT());
582 }
583
584 return $res;
585 }
586
587 sub list_users {
588 my ($self) = @_;
589
590 my $res = [];
591
592 my $dbh = $self->{dbstat}->{users}->{dbh};
593
594 return $res if !$dbh;
595
596 my $key = 0 ;
597 my $value = "" ;
598 my $status = $dbh->seq($key, $value, R_FIRST());
599 my $keys;
600
601 while ($status == 0) {
602 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $value);
603 push @$res, {
604 pmail => $pmail,
605 account => $account,
606 dn => $dn,
607 };
608 $status = $dbh->seq($key, $value, R_NEXT());
609 }
610
611 return $res;
612 }
613
614 sub list_addresses {
615 my ($self, $mail) = @_;
616
617 my $dbhmails = $self->{dbstat}->{mails}->{dbh};
618 my $dbhusers = $self->{dbstat}->{users}->{dbh};
619
620 return undef if !$dbhmails || !$dbhusers;
621
622 $mail = lc($mail);
623
624 my $res = [];
625
626 my $cuid;
627 $dbhmails->get($mail, $cuid);
628 return undef if !$cuid;
629
630 my $rdata;
631 $dbhusers->get($cuid, $rdata);
632 return undef if !$rdata;
633
634 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
635
636 push @$res, { primary => 1, email => $pmail };
637
638 my $key = 0 ;
639 my $value = "" ;
640 my $status = $dbhmails->seq($key, $value, R_FIRST());
641
642 while ($status == 0) {
643 if ($value == $cuid && $key ne $pmail) {
644 push @$res, { primary => 0, email => $key };
645 }
646 $status = $dbhmails->seq($key, $value, R_NEXT());
647 }
648
649 return $res;
650 }
651
652 sub mail_exists {
653 my ($self, $mail) = @_;
654
655 my $dbh = $self->{dbstat}->{mails}->{dbh};
656 return 0 if !$dbh;
657
658 $mail = lc($mail);
659
660 my $res;
661 $dbh->get($mail, $res);
662 return $res;
663 }
664
665 sub account_exists {
666 my ($self, $account) = @_;
667
668 my $dbh = $self->{dbstat}->{accounts}->{dbh};
669 return 0 if !$dbh;
670
671 $account = lc($account);
672
673 my $res;
674 $dbh->get($account, $res);
675 return $res;
676 }
677
678 sub group_exists {
679 my ($self, $group) = @_;
680
681 my $dbh = $self->{dbstat}->{groups}->{dbh};
682 return 0 if !$dbh;
683
684 my $res;
685 $dbh->get($group, $res);
686 return $res;
687 }
688
689 sub account_has_address {
690 my ($self, $account, $mail) = @_;
691
692 my $dbhmails = $self->{dbstat}->{mails}->{dbh};
693 my $dbhaccounts = $self->{dbstat}->{accounts}->{dbh};
694 return 0 if !$dbhmails || !$dbhaccounts;
695
696 $account = lc($account);
697 $mail = lc($mail);
698
699 my $accid;
700 $dbhaccounts->get($account, $accid);
701 return 0 if !$accid;
702
703 my $mailid;
704 $dbhmails->get($mail, $mailid);
705 return 0 if !$mailid;
706
707 return ($accid == $mailid);
708 }
709
710 sub user_in_group {
711 my ($self, $mail, $group) = @_;
712
713 my $dbhmails = $self->{dbstat}->{mails}->{dbh};
714 my $dbhgroups = $self->{dbstat}->{groups}->{dbh};
715 my $dbhmemberof = $self->{dbstat}->{memberof}->{dbh};
716
717 return 0 if !$dbhmails || !$dbhgroups || !$dbhmemberof;
718
719 $mail = lc($mail);
720
721 my $cuid;
722 $dbhmails->get($mail, $cuid);
723 return 0 if !$cuid;
724
725 my $groupid;
726 $dbhgroups->get($group, $groupid);
727 return 0 if !$groupid;
728
729 my @gida = $dbhmemberof->get_dup($cuid);
730
731 return grep { $_ eq $groupid } @gida;
732 }
733
734 sub account_info {
735 my ($self, $mail, $scan) = @_;
736
737 my $dbhmails = $self->{dbstat}->{mails}->{dbh};
738 my $dbhusers = $self->{dbstat}->{users}->{dbh};
739
740 return undef if !$dbhmails || !$dbhusers;
741
742 $mail = lc($mail);
743
744 my $res = {};
745
746 my $cuid;
747 $dbhmails->get($mail, $cuid);
748 return undef if !$cuid;
749
750 my $rdata;
751 $dbhusers->get($cuid, $rdata);
752 return undef if !$rdata;
753
754 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
755
756 $res->{dn} = $dn;
757 $res->{account} = $account;
758 $res->{pmail} = $pmail;
759
760 if ($scan) {
761 my $key = 0 ;
762 my $value = "" ;
763 my $status = $dbhmails->seq($key, $value, R_FIRST());
764 my $mails;
765
766 while ($status == 0) {
767 push @$mails, $key if $value == $cuid;
768 $status = $dbhmails->seq($key, $value, R_NEXT());
769 }
770 $res->{mails} = $mails;
771 }
772
773 return $res;
774 }
775
776 1;