]>
git.proxmox.com Git - pmg-api.git/blob - PMG/LDAPCache.pm
1 package PMG
::LDAPCache
;
8 use Net
::LDAP
::Control
::Paged
;
9 use Net
::LDAP
::Constant qw
(LDAP_CONTROL_PAGED
);
16 $DB_HASH->{'cachesize'} = 10000;
17 $DB_RECNO->{'cachesize'} = 10000;
18 $DB_BTREE->{'cachesize'} = 10000;
19 $DB_BTREE->{'flags'} = R_DUP
;
21 my $cachedir = '/var/lib/pmg';
28 # users (hash): UID -> pmail, account, DN
29 # dnames (hash): DN -> UID
30 # accounts (hash): account -> UID
31 # mail (hash): mail -> UID
32 # groups (hash): group -> GID
33 # memberof (btree): UID -> GID
35 my @dbs = ('users', 'dnames', 'groups', 'mails', 'accounts', 'memberof');
38 my ($self, %args) = @_;
40 my $type = ref($self) || $self;
42 die "undefined ldap id" if !$args{id
};
46 if ($ldapcache->{$id}) {
47 $self = $ldapcache->{$id};
49 $ldapcache->{$id} = $self = bless {}, $type;
53 if (!$args{mailattr
}) {
54 $args{mailattr
} = "mail, userPrincipalName, proxyAddresses, othermailbox";
56 $args{mailattr
} =~ s/[\,\;]/ /g;
57 $args{mailattr
} =~ s/\s+/,/g;
59 if ($args{mode
} && ($args{mode
} eq 'ldap' || $args{mode
} eq 'ldaps')) {
60 $self->{mode
} = $args{mode
};
62 $self->{mode
} = 'ldap';
65 $self->{accountattr
} = $args{accountattr
} || 'sAMAccountName';
66 @{$self->{mailattr
}} = split(/,/, $args{mailattr
});
67 $self->{server1
} = $args{server1
};
68 $self->{server2
} = $args{server2
};
69 $self->{binddn
} = $args{binddn
};
70 $self->{bindpw
} = $args{bindpw
};
71 $self->{basedn
} = $args{basedn
};
72 $self->{port
} = $args{port
};
73 $self->{groupbasedn
} = $args{groupbasedn
};
74 $self->{filter
} = $args{filter
};
76 if ($args{syncmode
} == 1) {
77 # read local data only
83 return $self if !($args{server1
});
85 if ($args{syncmode
} == 2) {
98 my $dir = "$cachedir/ldapdb_$id";
99 my $scheme = LockFile
::Simple-
>make(
100 -warn => 0, -stale
=> 1, -autoclean
=> 1);
101 my $lock = $scheme->lock($dir);
107 my ($class, $id) = @_;
109 if (my $lock = lockdir
($id)) {
110 delete $ldapcache->{$id};
111 delete $last_atime->{$id};
112 my $dir = "$cachedir/ldapdb_$id";
116 syslog
('err' , "can't lock ldap database '$id'");
121 my ($self, $syncmode) = @_;
123 if ($syncmode == 1) {
124 # read local data only
125 $self->{errors
} = '';
127 } elsif ($syncmode == 2) {
136 my ($self, $ldap) = @_;
139 foreach my $attr (@{$self->{mailattr
}}) {
140 $filter .= "($attr=*)";
144 if ($self->{filter
}) {
145 my $tmp = $self->{filter
};
146 $tmp = "($tmp)" if $tmp !~ m/^\(.*\)$/;
148 $filter = "(&${filter}${tmp})";
151 my $page = Net
::LDAP
::Control
::Paged-
>new(size
=> 900);
154 base
=> $self->{basedn
},
157 control
=> [ $page ],
158 attrs
=> [ @{$self->{mailattr
}}, $self->{accountattr
}, 'memberOf' ]
165 my $mesg = $ldap->search(@args);
169 my $err = "ldap user search error: " . $mesg->error;
170 $self->{errors
} .= "$err\n";
175 #foreach my $entry ($mesg->entries) { $entry->dump; }
176 foreach my $entry ($mesg->entries) {
182 foreach my $attr (@{$self->{mailattr
}}) {
183 foreach my $mail ($entry->get_value($attr)) {
185 # Test if the Line starts with one of the following lines:
186 # proxyAddresses: [smtp|SMTP]:
187 # and also discard this starting string, so that $mail is only the
188 # address without any other characters...
190 $mail =~ s/^(smtp|SMTP)[\:\$]//gs;
192 if ($mail !~ m/[\{\}\\\/]/ && $mail =~ m/^\S
+\
@\S+$/) {
193 $umails->{$mail} = 1;
194 $pmail = $mail if !$pmail;
198 my $addresses = [ keys %$umails ];
200 next if !$pmail; # account has no email addresses
203 $self->{dbstat
}->{dnames
}->{dbh
}->get($dn, $cuid);
205 $cuid = ++$self->{dbstat
}->{dnames
}->{idcount
};
206 $self->{dbstat
}->{dnames
}->{dbh
}->put($dn, $cuid);
209 my $account = $entry->get_value($self->{accountattr
});
210 if ($account && ($account =~ m/^\S+$/s)) {
211 $account = lc($account);
212 $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);
220 foreach my $mail (@$addresses) {
221 $self->{dbstat
}->{mails
}->{dbh
}->put($mail, $cuid);
224 if (!$self->{groupbasedn
}) {
225 my @groups = $entry->get_value('memberOf');
226 foreach my $group (@groups) {
228 $self->{dbstat
}->{groups
}->{dbh
}->get($group, $cgid);
230 $cgid = ++$self->{dbstat
}->{groups
}->{idcount
};
231 $self->{dbstat
}->{groups
}->{dbh
}->put($group, $cgid);
233 $self->{dbstat
}->{memberof
}->{dbh
}->put($cuid, $cgid);
238 # Get cookie from paged control
239 my ($resp) = $mesg->control(LDAP_CONTROL_PAGED
) or last;
240 $cookie = $resp->cookie or last;
242 # Set cookie in paged control
243 $page->cookie($cookie);
248 # We had an abnormal exit, so let the server know we do not want any more
249 $page->cookie($cookie);
251 $ldap->search(@args);
252 my $err = "LDAP user query unsuccessful";
253 $self->{errors
} .= "$err\n";
259 my ($self, $ldap) = @_;
261 return undef if !$self->{groupbasedn
};
263 my $filter = "(objectclass=group)";
265 my $page = Net
::LDAP
::Control
::Paged-
>new(size
=> 100);
267 my @args = ( base
=> $self->{groupbasedn
},
270 control
=> [ $page ],
271 attrs
=> [ 'member' ]
277 my $mesg = $ldap->search(@args);
281 my $err = "ldap group search error: " . $mesg->error;
282 $self->{errors
} .= "$err\n";
287 foreach my $entry ( $mesg->entries ) {
288 my $group = $entry->dn;
289 my @members = $entry->get_value('member');
292 $self->{dbstat
}->{groups
}->{dbh
}->get($group, $cgid);
294 $cgid = ++$self->{dbstat
}->{groups
}->{idcount
};
295 $self->{dbstat
}->{groups
}->{dbh
}->put($group, $cgid);
298 foreach my $m (@members) {
301 $self->{dbstat
}->{dnames
}->{dbh
}->get($m, $cuid);
303 $cuid = ++$self->{dbstat
}->{dnames
}->{idcount
};
304 $self->{dbstat
}->{dnames
}->{dbh
}->put($m, $cuid);
307 $self->{dbstat
}->{memberof
}->{dbh
}->put($cuid, $cgid);
311 # Get cookie from paged control
312 my ($resp) = $mesg->control(LDAP_CONTROL_PAGED
) or last;
313 $cookie = $resp->cookie or last;
315 # Set cookie in paged control
316 $page->cookie($cookie);
320 # We had an abnormal exit, so let the server know we do not want any more
321 $page->cookie($cookie);
323 $ldap->search(@args);
324 my $err = "LDAP group query unsuccessful";
325 $self->{errors
} .= "$err\n";
333 my $mode = $self->{mode
};
335 $portstr = ':' . $self->{port
} if $self->{port
};
337 my $serverstr = "$mode://$self->{server1}${portstr}/";
338 my $ldap = Net
::LDAP-
>new($serverstr);
339 if(!$ldap && $self->{server2
} && $self->{server2
} ne '127.0.0.1') {
340 $serverstr = "$mode://$self->{server2}${portstr}/";
341 $ldap = Net
::LDAP-
>new($serverstr);
350 my $dir = "ldapdb_" . $self->{id
};
351 mkdir "$cachedir/$dir";
353 # open ldap connection
355 syslog
('info', "syncing ldap database '$self->{id}'");
357 my $ldap = $self->ldap_connect();
360 my $err = "Can't bind to ldap server '$self->{id}': $!";
361 $self->{errors
} .= "$err\n";
368 if ($self->{binddn
}) {
369 $mesg = $ldap->bind($self->{binddn
}, password
=> $self->{bindpw
});
371 $mesg = $ldap->bind(); # anonymous bind
375 my $err = "ldap bind failed: " . $mesg->error;
376 $self->{errors
} .= "$err\n";
381 if (!$self->{basedn
}) {
382 my $root = $ldap->root_dse(attrs
=> [ 'defaultNamingContext' ]);
383 $self->{basedn
} = $root->get_value('defaultNamingContext');
386 # open temporary database files
390 foreach my $db (@dbs) {
391 $self->{dbstat
}->{$db}->{tmpfilename
} = "$cachedir/$dir/${db}_tmp$$.db";
392 $olddbh->{$db} = $self->{dbstat
}->{$db}->{dbh
};
396 foreach my $db (@dbs) {
397 my $filename = $self->{dbstat
}->{$db}->{tmpfilename
};
398 $self->{dbstat
}->{$db}->{idcount
} = 0;
401 if ($db eq 'memberof') {
402 $self->{dbstat
}->{$db}->{dbh
} =
403 tie
(my %h, 'DB_File', $filename,
404 O_CREAT
|O_RDWR
, 0666, $DB_BTREE);
406 $self->{dbstat
}->{$db}->{dbh
} =
407 tie
(my %h, 'DB_File', $filename,
408 O_CREAT
|O_RDWR
, 0666, $DB_HASH);
411 die "unable to open database file '$filename': $!\n"
412 if !$self->{dbstat
}->{$db}->{dbh
};
419 # close and delete all files
420 foreach my $db (@dbs) {
421 undef $self->{dbstat
}->{$db}->{dbh
};
422 unlink $self->{dbstat
}->{$db}->{tmpfilename
};
423 $self->{dbstat
}->{$db}->{dbh
} = $olddbh->{$db};
425 $self->{errors
} .= $err;
431 $self->querygroups ($ldap) if $self->{groupbasedn
};
433 if (!$self->{errors
}) {
434 $self->queryusers($ldap);
439 if ($self->{errors
}) {
440 # close and delete all files
441 foreach my $db (@dbs) {
442 undef $self->{dbstat
}->{$db}->{dbh
};
443 unlink $self->{dbstat
}->{$db}->{tmpfilename
};
444 $self->{dbstat
}->{$db}->{dbh
} = $olddbh->{$db};
448 my $lock = lockdir
($self->{id
});
451 my $err = "unable to get database lock for ldap database '$self->{id}'";
452 $self->{errors
} .= "$err\n";
455 # close and delete all files
456 foreach my $db (@dbs) {
457 undef $self->{dbstat
}->{$db}->{dbh
};
458 unlink $self->{dbstat
}->{$db}->{tmpfilename
};
459 $self->{dbstat
}->{$db}->{dbh
} = $olddbh->{$db};
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;
471 $last_atime->{$self->{id
}} = time();
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
});
480 sub __count_entries
{
488 my $status = $dbh->seq($key, $value, R_FIRST
());
490 while ($status == 0) {
492 $status = $dbh->seq($key, $value, R_NEXT
());
499 my ($self, $try) = @_;
501 my $dir = "ldapdb_" . $self->{id
};
502 mkdir "$cachedir/$dir";
504 my $filename = "$cachedir/$dir/mails.db";
506 return if $last_atime->{$self->{id
}} &&
507 PMG
::Utils
::file_older_than
($filename, $last_atime->{$self->{id
}});
510 foreach my $db (@dbs) {
511 my $filename = $self->{dbstat
}->{$db}->{filename
} =
512 "$cachedir/$dir/${db}.db";
513 $self->{dbstat
}->{$db}->{idcount
} = 0;
514 if ($db eq 'memberof') {
515 $self->{dbstat
}->{$db}->{dbh
} =
516 tie
(my %h, 'DB_File', $filename,
517 O_RDONLY
, 0666, $DB_BTREE);
519 $self->{dbstat
}->{$db}->{dbh
} =
520 tie
(my %h, 'DB_File', $filename,
521 O_RDONLY
, 0666, $DB_HASH);
524 if (!$self->{dbstat
}->{$db}->{dbh
} && !$try) {
525 my $err = "ldap error - unable to open database file '$filename': $!";
526 $self->{errors
} .= "$err\n";
527 syslog
('err', $err) if !$self->{dbstat
}->{$db}->{dbh
};
532 $last_atime->{$self->{id
}} = time();
534 $self->{gcount
} = __count_entries
($self->{dbstat
}->{groups
}->{dbh
});
535 $self->{ucount
} = __count_entries
($self->{dbstat
}->{accounts
}->{dbh
});
536 $self->{mcount
} = __count_entries
($self->{dbstat
}->{mails
}->{dbh
});
540 my ($self, $force) = @_;
542 $self->{errors
} = '';
545 # only sync if file is older than 1 hour
547 my $dir = "ldapdb_" . $self->{id
};
548 mkdir "$cachedir/$dir";
549 my $filename = "$cachedir/$dir/mails.db";
552 !PMG
::Utils
::file_older_than
($filename, time() - 3600)) {
558 $self->sync_database();
560 if ($self->{errors
}) {
568 my $dbh = $self->{dbstat
}->{groups
}->{dbh
};
573 my $status = $dbh->seq($key, $value, R_FIRST
());
576 while ($status == 0) {
578 $status = $dbh->seq($key, $value, R_NEXT
());
585 my ($self, $mail) = @_;
587 my $dbh = $self->{dbstat
}->{mails
}->{dbh
};
593 $dbh->get($mail, $res);
598 my ($self, $account) = @_;
600 my $dbh = $self->{dbstat
}->{accounts
}->{dbh
};
603 $account = lc($account);
606 $dbh->get($account, $res);
610 sub account_has_address
{
611 my ($self, $account, $mail) = @_;
613 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
614 my $dbhaccounts = $self->{dbstat
}->{accounts
}->{dbh
};
615 return 0 if !$dbhmails || !$dbhaccounts;
617 $account = lc($account);
621 $dbhaccounts->get($account, $accid);
625 $dbhmails->get($mail, $mailid);
626 return 0 if !$mailid;
628 return ($accid == $mailid);
632 my ($self, $mail, $group) = @_;
634 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
635 my $dbhgroups = $self->{dbstat
}->{groups
}->{dbh
};
636 my $dbhmemberof = $self->{dbstat
}->{memberof
}->{dbh
};
638 return 0 if !$dbhmails || !$dbhgroups || !$dbhmemberof;
643 $dbhmails->get($mail, $cuid);
647 $dbhgroups->get($group, $groupid);
648 return 0 if !$groupid;
650 my @gida = $dbhmemberof->get_dup($cuid);
652 return grep { $_ eq $groupid } @gida;
656 my ($self, $mail, $scan) = @_;
658 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
659 my $dbhusers = $self->{dbstat
}->{users
}->{dbh
};
661 return undef if !$dbhmails || !$dbhusers;
668 $dbhmails->get($mail, $cuid);
669 return undef if !$cuid;
672 $dbhusers->get($cuid, $rdata);
673 return undef if !$rdata;
675 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
678 $res->{account
} = $account;
679 $res->{pmail
} = $pmail;
684 my $status = $dbhmails->seq($key, $value, R_FIRST
());
687 while ($status == 0) {
688 push @$mails, $key if $value == $cuid;
689 $status = $dbhmails->seq($key, $value, R_NEXT
());
691 $res->{mails
} = $mails;