]>
git.proxmox.com Git - pmg-api.git/blob - PMG/LDAPCache.pm
1 package PMG
::LDAPCache
;
9 use Net
::LDAP
::Control
::Paged
;
10 use Net
::LDAP
::Constant qw
(LDAP_CONTROL_PAGED
);
17 $DB_HASH->{'cachesize'} = 10000;
18 $DB_RECNO->{'cachesize'} = 10000;
19 $DB_BTREE->{'cachesize'} = 10000;
20 $DB_BTREE->{'flags'} = R_DUP
;
22 my $cachedir = '/var/lib/proxmox-mailgateway';
29 # users (hash): UID -> pmail, account, DN
30 # dnames (hash): DN -> UID
31 # accounts (hash): account -> UID
32 # mail (hash): mail -> UID
33 # groups (hash): group -> GID
34 # memberof (btree): UID -> GID
36 my @dbs = ('users', 'dnames', 'groups', 'mails', 'accounts', 'memberof');
41 my $type = ref($self) || $self;
45 die "undefine ldap id" if !$args{id
};
50 if ($ldapcache->{$id}) {
51 $self = $ldapcache->{$id};
53 $ldapcache->{$id} = $self = bless {}, $type;
57 if (!$args{mailattr
}) {
58 $args{mailattr
} = "mail, userPrincipalName, proxyAddresses, othermailbox";
60 $args{mailattr
} =~ s/[\,\;]/ /g;
61 $args{mailattr
} =~ s/\s+/,/g;
63 if ($args{mode
} && ($args{mode
} eq 'ldap' || $args{mode
} eq 'ldaps')) {
64 $self->{mode
} = $args{mode
};
66 $self->{mode
} = 'ldap';
69 $self->{accountattr
} = $args{accountattr
} || 'sAMAccountName';
70 @{$self->{mailattr
}} = split(/,/, $args{mailattr
});
71 $self->{server1
} = $args{server1
};
72 $self->{server2
} = $args{server2
};
73 $self->{binddn
} = $args{binddn
};
74 $self->{bindpw
} = $args{bindpw
};
75 $self->{basedn
} = $args{basedn
};
76 $self->{port
} = $args{port
};
77 $self->{groupbasedn
} = $args{groupbasedn
};
78 $self->{filter
} = $args{filter
};
80 if ($args{syncmode
} == 1) {
81 # read local data only
87 return $self if !($args{server1
});
89 if ($args{syncmode
} == 2) {
102 my $dir = "$cachedir/ldapdb_$id";
103 my $scheme = LockFile
::Simple-
>make(
104 -warn => 0, -stale
=> 1, -autoclean
=> 1);
105 my $lock = $scheme->lock($dir);
111 my ($class, $id) = @_;
115 if (my $lock = lockdir
($id)) {
116 delete $ldapcache->{$id};
117 delete $last_atime->{$id};
118 my $dir = "$cachedir/ldapdb_$id";
122 syslog
('err' , "can't lock ldap database '$id'");
127 my ($self, $syncmode) = @_;
129 if ($syncmode == 1) {
130 # read local data only
131 $self->{errors
} = '';
133 } elsif ($syncmode == 2) {
142 my ($self, $ldap) = @_;
145 foreach my $attr (@{$self->{mailattr
}}) {
146 $filter .= "($attr=*)";
150 if ($self->{filter
}) {
151 my $tmp = $self->{filter
};
152 $tmp = "($tmp)" if $tmp !~ m/^\(.*\)$/;
154 $filter = "(&${filter}${tmp})";
157 my $page = Net
::LDAP
::Control
::Paged-
>new(size
=> 900);
160 base
=> $self->{basedn
},
163 control
=> [ $page ],
164 attrs
=> [ @{$self->{mailattr
}}, $self->{accountattr
}, 'memberOf' ]
171 my $mesg = $ldap->search(@args);
175 my $err = "ldap user search error: " . $mesg->error;
176 $self->{errors
} .= "$err\n";
181 #foreach my $entry ($mesg->entries) { $entry->dump; }
182 foreach my $entry ($mesg->entries) {
188 foreach my $attr (@{$self->{mailattr
}}) {
189 foreach my $mail ($entry->get_value($attr)) {
191 # Test if the Line starts with one of the following lines:
192 # proxyAddresses: [smtp|SMTP]:
193 # and also discard this starting string, so that $mail is only the
194 # address without any other characters...
196 $mail =~ s/^(smtp|SMTP)[\:\$]//gs;
198 if ($mail !~ m/[\{\}\\\/]/ && $mail =~ m/^\S
+\
@\S+$/) {
199 $umails->{$mail} = 1;
200 $pmail = $mail if !$pmail;
204 my $addresses = [ keys %$umails ];
206 next if !$pmail; # account has no email addresses
209 $self->{dbstat
}->{dnames
}->{dbh
}->get($dn, $cuid);
211 $cuid = ++$self->{dbstat
}->{dnames
}->{idcount
};
212 $self->{dbstat
}->{dnames
}->{dbh
}->put($dn, $cuid);
215 my $account = $entry->get_value($self->{accountattr
});
216 if ($account && ($account =~ m/^\S+$/s)) {
217 $account = lc($account);
218 $self->{dbstat
}->{accounts
}->{dbh
}->put($account, $cuid);
223 my $data = pack('n/a* n/a* n/a*', $pmail, $account, $dn);
224 $self->{dbstat
}->{users
}->{dbh
}->put($cuid, $data);
226 foreach my $mail (@$addresses) {
227 $self->{dbstat
}->{mails
}->{dbh
}->put($mail, $cuid);
230 if (!$self->{groupbasedn
}) {
231 my @groups = $entry->get_value('memberOf');
232 foreach my $group (@groups) {
234 $self->{dbstat
}->{groups
}->{dbh
}->get($group, $cgid);
236 $cgid = ++$self->{dbstat
}->{groups
}->{idcount
};
237 $self->{dbstat
}->{groups
}->{dbh
}->put($group, $cgid);
239 $self->{dbstat
}->{memberof
}->{dbh
}->put($cuid, $cgid);
244 # Get cookie from paged control
245 my ($resp) = $mesg->control(LDAP_CONTROL_PAGED
) or last;
246 $cookie = $resp->cookie or last;
248 # Set cookie in paged control
249 $page->cookie($cookie);
254 # We had an abnormal exit, so let the server know we do not want any more
255 $page->cookie($cookie);
257 $ldap->search(@args);
258 my $err = "LDAP user query unsuccessful";
259 $self->{errors
} .= "$err\n";
265 my ($self, $ldap) = @_;
267 return undef if !$self->{groupbasedn
};
269 my $filter = "(objectclass=group)";
271 my $page = Net
::LDAP
::Control
::Paged-
>new(size
=> 100);
273 my @args = ( base
=> $self->{groupbasedn
},
276 control
=> [ $page ],
277 attrs
=> [ 'member' ]
283 my $mesg = $ldap->search(@args);
287 my $err = "ldap group search error: " . $mesg->error;
288 $self->{errors
} .= "$err\n";
293 foreach my $entry ( $mesg->entries ) {
294 my $group = $entry->dn;
295 my @members = $entry->get_value('member');
298 $self->{dbstat
}->{groups
}->{dbh
}->get($group, $cgid);
300 $cgid = ++$self->{dbstat
}->{groups
}->{idcount
};
301 $self->{dbstat
}->{groups
}->{dbh
}->put($group, $cgid);
304 foreach my $m (@members) {
307 $self->{dbstat
}->{dnames
}->{dbh
}->get($m, $cuid);
309 $cuid = ++$self->{dbstat
}->{dnames
}->{idcount
};
310 $self->{dbstat
}->{dnames
}->{dbh
}->put($m, $cuid);
313 $self->{dbstat
}->{memberof
}->{dbh
}->put($cuid, $cgid);
317 # Get cookie from paged control
318 my ($resp) = $mesg->control(LDAP_CONTROL_PAGED
) or last;
319 $cookie = $resp->cookie or last;
321 # Set cookie in paged control
322 $page->cookie($cookie);
326 # We had an abnormal exit, so let the server know we do not want any more
327 $page->cookie($cookie);
329 $ldap->search(@args);
330 my $err = "LDAP group query unsuccessful";
331 $self->{errors
} .= "$err\n";
339 my $mode = $self->{mode
};
341 $portstr = ':' . $self->{port
} if $self->{port
};
343 my $serverstr = "$mode://$self->{server1}${portstr}/";
344 my $ldap = Net
::LDAP-
>new($serverstr);
345 if(!$ldap && $self->{server2
} && $self->{server2
} ne '127.0.0.1') {
346 $serverstr = "$mode://$self->{server2}${portstr}/";
347 $ldap = Net
::LDAP-
>new($serverstr);
356 my $dir = "ldapdb_" . $self->{id
};
357 mkdir "$cachedir/$dir";
359 # open ldap connection
361 syslog
('info', "syncing ldap database '$self->{id}'");
363 my $ldap = $self->ldap_connect();
366 my $err = "Can't bind to ldap server '$self->{id}': $!";
367 $self->{errors
} .= "$err\n";
374 if ($self->{binddn
}) {
375 $mesg = $ldap->bind($self->{binddn
}, password
=> $self->{bindpw
});
377 $mesg = $ldap->bind(); # anonymous bind
381 my $err = "ldap bind failed: " . $mesg->error;
382 $self->{errors
} .= "$err\n";
387 if (!$self->{basedn
}) {
388 my $root = $ldap->root_dse(attrs
=> [ 'defaultNamingContext' ]);
389 $self->{basedn
} = $root->get_value('defaultNamingContext');
392 # open temporary database files
396 foreach my $db (@dbs) {
397 $self->{dbstat
}->{$db}->{tmpfilename
} = "$cachedir/$dir/${db}_tmp$$.db";
398 $olddbh->{$db} = $self->{dbstat
}->{$db}->{dbh
};
402 foreach my $db (@dbs) {
403 my $filename = $self->{dbstat
}->{$db}->{tmpfilename
};
404 $self->{dbstat
}->{$db}->{idcount
} = 0;
407 if ($db eq 'memberof') {
408 $self->{dbstat
}->{$db}->{dbh
} =
409 tie
(my %h, 'DB_File', $filename,
410 O_CREAT
|O_RDWR
, 0666, $DB_BTREE);
412 $self->{dbstat
}->{$db}->{dbh
} =
413 tie
(my %h, 'DB_File', $filename,
414 O_CREAT
|O_RDWR
, 0666, $DB_HASH);
417 die "unable to open database file '$filename': $!\n"
418 if !$self->{dbstat
}->{$db}->{dbh
};
425 # close and delete all files
426 foreach my $db (@dbs) {
427 undef $self->{dbstat
}->{$db}->{dbh
};
428 unlink $self->{dbstat
}->{$db}->{tmpfilename
};
429 $self->{dbstat
}->{$db}->{dbh
} = $olddbh->{$db};
431 $self->{errors
} .= $err;
437 $self->querygroups ($ldap) if $self->{groupbasedn
};
439 if (!$self->{errors
}) {
440 $self->queryusers($ldap);
445 if ($self->{errors
}) {
446 # close and delete all files
447 foreach my $db (@dbs) {
448 undef $self->{dbstat
}->{$db}->{dbh
};
449 unlink $self->{dbstat
}->{$db}->{tmpfilename
};
450 $self->{dbstat
}->{$db}->{dbh
} = $olddbh->{$db};
454 my $lock = lockdir
($self->{id
});
457 my $err = "unable to get database lock for ldap database '$self->{id}'";
458 $self->{errors
} .= "$err\n";
461 # close and delete all files
462 foreach my $db (@dbs) {
463 undef $self->{dbstat
}->{$db}->{dbh
};
464 unlink $self->{dbstat
}->{$db}->{tmpfilename
};
465 $self->{dbstat
}->{$db}->{dbh
} = $olddbh->{$db};
468 foreach my $db (@dbs) {
469 my $filename = $self->{dbstat
}->{$db}->{filename
} =
470 "$cachedir/$dir/${db}.db";
471 $self->{dbstat
}->{$db}->{dbh
}->sync(); # flush everything
472 rename $self->{dbstat
}->{$db}->{tmpfilename
}, $filename;
477 $last_atime->{$self->{id
}} = time();
479 $self->{gcount
} = $self->{dbstat
}->{groups
}->{idcount
};
480 $self->{ucount
} = __count_entries
($self->{dbstat
}->{accounts
}->{dbh
});
481 $self->{mcount
} = __count_entries
($self->{dbstat
}->{mails
}->{dbh
});
486 sub __count_entries
{
494 my $status = $dbh->seq($key, $value, R_FIRST
());
496 while ($status == 0) {
498 $status = $dbh->seq($key, $value, R_NEXT
());
505 my ($self, $try) = @_;
507 my $dir = "ldapdb_" . $self->{id
};
508 mkdir "$cachedir/$dir";
510 my $filename = "$cachedir/$dir/mails.db";
512 return if $last_atime->{$self->{id
}} &&
513 PMG
::Utils
::file_older_than
($filename, $last_atime->{$self->{id
}});
516 foreach my $db (@dbs) {
517 my $filename = $self->{dbstat
}->{$db}->{filename
} =
518 "$cachedir/$dir/${db}.db";
519 $self->{dbstat
}->{$db}->{idcount
} = 0;
520 if ($db eq 'memberof') {
521 $self->{dbstat
}->{$db}->{dbh
} =
522 tie
(my %h, 'DB_File', $filename,
523 O_RDONLY
, 0666, $DB_BTREE);
525 $self->{dbstat
}->{$db}->{dbh
} =
526 tie
(my %h, 'DB_File', $filename,
527 O_RDONLY
, 0666, $DB_HASH);
530 if (!$self->{dbstat
}->{$db}->{dbh
} && !$try) {
531 my $err = "ldap error - unable to open database file '$filename': $!";
532 $self->{errors
} .= "$err\n";
533 syslog
('err', $err) if !$self->{dbstat
}->{$db}->{dbh
};
538 $last_atime->{$self->{id
}} = time();
540 $self->{gcount
} = __count_entries
($self->{dbstat
}->{groups
}->{dbh
});
541 $self->{ucount
} = __count_entries
($self->{dbstat
}->{accounts
}->{dbh
});
542 $self->{mcount
} = __count_entries
($self->{dbstat
}->{mails
}->{dbh
});
546 my ($self, $force) = @_;
548 $self->{errors
} = '';
551 # only sync if file is older than 1 hour
553 my $dir = "ldapdb_" . $self->{id
};
554 mkdir "$cachedir/$dir";
555 my $filename = "$cachedir/$dir/mails.db";
558 !PMG
::Utils
::file_older_than
($filename, time() - 3600)) {
564 $self->sync_database();
566 if ($self->{errors
}) {
574 my $dbh = $self->{dbstat
}->{groups
}->{dbh
};
579 my $status = $dbh->seq($key, $value, R_FIRST
());
582 while ($status == 0) {
584 $status = $dbh->seq($key, $value, R_NEXT
());
591 my ($self, $mail) = @_;
593 my $dbh = $self->{dbstat
}->{mails
}->{dbh
};
599 $dbh->get($mail, $res);
604 my ($self, $account) = @_;
606 my $dbh = $self->{dbstat
}->{accounts
}->{dbh
};
609 $account = lc($account);
612 $dbh->get($account, $res);
616 sub account_has_address
{
617 my ($self, $account, $mail) = @_;
619 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
620 my $dbhaccounts = $self->{dbstat
}->{accounts
}->{dbh
};
621 return 0 if !$dbhmails || !$dbhaccounts;
623 $account = lc($account);
627 $dbhaccounts->get($account, $accid);
631 $dbhmails->get($mail, $mailid);
632 return 0 if !$mailid;
634 return ($accid == $mailid);
638 my ($self, $mail, $group) = @_;
640 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
641 my $dbhgroups = $self->{dbstat
}->{groups
}->{dbh
};
642 my $dbhmemberof = $self->{dbstat
}->{memberof
}->{dbh
};
644 return 0 if !$dbhmails || !$dbhgroups || !$dbhmemberof;
649 $dbhmails->get($mail, $cuid);
653 $dbhgroups->get($group, $groupid);
654 return 0 if !$groupid;
656 my @gida = $dbhmemberof->get_dup($cuid);
658 return grep { $_ eq $groupid } @gida;
662 my ($self, $mail, $scan) = @_;
664 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
665 my $dbhusers = $self->{dbstat
}->{users
}->{dbh
};
667 return undef if !$dbhmails || !$dbhusers;
674 $dbhmails->get($mail, $cuid);
675 return undef if !$cuid;
678 $dbhusers->get($cuid, $rdata);
679 return undef if !$rdata;
681 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
684 $res->{account
} = $account;
685 $res->{pmail
} = $pmail;
690 my $status = $dbhmails->seq($key, $value, R_FIRST
());
693 while ($status == 0) {
694 push @$mails, $key if $value == $cuid;
695 $status = $dbhmails->seq($key, $value, R_NEXT
());
697 $res->{mails
} = $mails;