]>
git.proxmox.com Git - pmg-api.git/blob - PMG/LDAPCache.pm
b361eee3f19ef840418e97c60ebb1a9111fe6518
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/pmg';
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');
39 my ($self, %args) = @_;
41 my $type = ref($self) || $self;
43 die "undefined ldap id" if !$args{id
};
47 if ($ldapcache->{$id}) {
48 $self = $ldapcache->{$id};
50 $ldapcache->{$id} = $self = bless {}, $type;
54 if (!$args{mailattr
}) {
55 $args{mailattr
} = "mail, userPrincipalName, proxyAddresses, othermailbox";
57 $args{mailattr
} =~ s/[\,\;]/ /g;
58 $args{mailattr
} =~ s/\s+/,/g;
60 if ($args{mode
} && ($args{mode
} eq 'ldap' || $args{mode
} eq 'ldaps')) {
61 $self->{mode
} = $args{mode
};
63 $self->{mode
} = 'ldap';
66 $self->{accountattr
} = $args{accountattr
} || 'sAMAccountName';
67 @{$self->{mailattr
}} = split(/,/, $args{mailattr
});
68 $self->{server1
} = $args{server1
};
69 $self->{server2
} = $args{server2
};
70 $self->{binddn
} = $args{binddn
};
71 $self->{bindpw
} = $args{bindpw
};
72 $self->{basedn
} = $args{basedn
};
73 $self->{port
} = $args{port
};
74 $self->{groupbasedn
} = $args{groupbasedn
};
75 $self->{filter
} = $args{filter
};
77 if ($args{syncmode
} == 1) {
78 # read local data only
84 return $self if !($args{server1
});
86 if ($args{syncmode
} == 2) {
99 my $dir = "$cachedir/ldapdb_$id";
100 my $scheme = LockFile
::Simple-
>make(
101 -warn => 0, -stale
=> 1, -autoclean
=> 1);
102 my $lock = $scheme->lock($dir);
108 my ($class, $id) = @_;
110 if (my $lock = lockdir
($id)) {
111 delete $ldapcache->{$id};
112 delete $last_atime->{$id};
113 my $dir = "$cachedir/ldapdb_$id";
117 syslog
('err' , "can't lock ldap database '$id'");
122 my ($self, $syncmode) = @_;
124 if ($syncmode == 1) {
125 # read local data only
126 $self->{errors
} = '';
128 } elsif ($syncmode == 2) {
137 my ($self, $ldap) = @_;
140 foreach my $attr (@{$self->{mailattr
}}) {
141 $filter .= "($attr=*)";
145 if ($self->{filter
}) {
146 my $tmp = $self->{filter
};
147 $tmp = "($tmp)" if $tmp !~ m/^\(.*\)$/;
149 $filter = "(&${filter}${tmp})";
152 my $page = Net
::LDAP
::Control
::Paged-
>new(size
=> 900);
155 base
=> $self->{basedn
},
158 control
=> [ $page ],
159 attrs
=> [ @{$self->{mailattr
}}, $self->{accountattr
}, 'memberOf' ]
166 my $mesg = $ldap->search(@args);
170 my $err = "ldap user search error: " . $mesg->error;
171 $self->{errors
} .= "$err\n";
176 #foreach my $entry ($mesg->entries) { $entry->dump; }
177 foreach my $entry ($mesg->entries) {
183 foreach my $attr (@{$self->{mailattr
}}) {
184 foreach my $mail ($entry->get_value($attr)) {
186 # Test if the Line starts with one of the following lines:
187 # proxyAddresses: [smtp|SMTP]:
188 # and also discard this starting string, so that $mail is only the
189 # address without any other characters...
191 $mail =~ s/^(smtp|SMTP)[\:\$]//gs;
193 if ($mail !~ m/[\{\}\\\/]/ && $mail =~ m/^\S
+\
@\S+$/) {
194 $umails->{$mail} = 1;
195 $pmail = $mail if !$pmail;
199 my $addresses = [ keys %$umails ];
201 next if !$pmail; # account has no email addresses
204 $self->{dbstat
}->{dnames
}->{dbh
}->get($dn, $cuid);
206 $cuid = ++$self->{dbstat
}->{dnames
}->{idcount
};
207 $self->{dbstat
}->{dnames
}->{dbh
}->put($dn, $cuid);
210 my $account = $entry->get_value($self->{accountattr
});
211 if ($account && ($account =~ m/^\S+$/s)) {
212 $account = lc($account);
213 $self->{dbstat
}->{accounts
}->{dbh
}->put($account, $cuid);
214 my $data = pack('n/a* n/a* n/a*', $pmail, $account, $dn);
215 $self->{dbstat
}->{users
}->{dbh
}->put($cuid, $data);
218 foreach my $mail (@$addresses) {
219 $self->{dbstat
}->{mails
}->{dbh
}->put($mail, $cuid);
222 if (!$self->{groupbasedn
}) {
223 my @groups = $entry->get_value('memberOf');
224 foreach my $group (@groups) {
226 $self->{dbstat
}->{groups
}->{dbh
}->get($group, $cgid);
228 $cgid = ++$self->{dbstat
}->{groups
}->{idcount
};
229 $self->{dbstat
}->{groups
}->{dbh
}->put($group, $cgid);
231 $self->{dbstat
}->{memberof
}->{dbh
}->put($cuid, $cgid);
236 # Get cookie from paged control
237 my ($resp) = $mesg->control(LDAP_CONTROL_PAGED
) or last;
238 $cookie = $resp->cookie or last;
240 # Set cookie in paged control
241 $page->cookie($cookie);
246 # We had an abnormal exit, so let the server know we do not want any more
247 $page->cookie($cookie);
249 $ldap->search(@args);
250 my $err = "LDAP user query unsuccessful";
251 $self->{errors
} .= "$err\n";
257 my ($self, $ldap) = @_;
259 return undef if !$self->{groupbasedn
};
261 my $filter = "(|(objectclass=group)(objectclass=univentionGroup))";
263 my $page = Net
::LDAP
::Control
::Paged-
>new(size
=> 100);
265 my @args = ( base
=> $self->{groupbasedn
},
268 control
=> [ $page ],
269 attrs
=> [ 'member', 'uniqueMember' ],
275 my $mesg = $ldap->search(@args);
279 my $err = "ldap group search error: " . $mesg->error;
280 $self->{errors
} .= "$err\n";
285 foreach my $entry ( $mesg->entries ) {
286 my $group = $entry->dn;
287 my @members = $entry->get_value('member');
288 if (!scalar(@members)) {
289 @members = $entry->get_value('uniqueMember');
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 $hosts = [ $self->{server1
} ];
335 push @$hosts, $self->{server2
} if $self->{server2
};
337 my $opts = { timeout
=> 10, onerror
=> 'die' };
339 $opts->{port
} = $self->{port
} if $self->{port
};
340 $opts->{schema
} = $self->{mode
};
342 return Net
::LDAP-
>new($hosts, %$opts);
345 sub ldap_connect_and_bind
{
348 my $ldap = $self->ldap_connect() ||
349 die "Can't bind to ldap server '$self->{id}': $!\n";
353 if ($self->{binddn
}) {
354 $mesg = $ldap->bind($self->{binddn
}, password
=> $self->{bindpw
});
356 $mesg = $ldap->bind(); # anonymous bind
359 die "ldap bind failed: " . $mesg->error . "\n" if $mesg->code;
361 if (!$self->{basedn
}) {
362 my $root = $ldap->root_dse(attrs
=> [ 'defaultNamingContext' ]);
363 $self->{basedn
} = $root->get_value('defaultNamingContext');
372 my $dir = "ldapdb_" . $self->{id
};
373 mkdir "$cachedir/$dir";
375 # open ldap connection
379 eval { $ldap = $self->ldap_connect_and_bind(); };
381 $self->{errors
} .= "$err\n";
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
};
395 my $error_cleanup = sub {
396 # close and delete all files
397 foreach my $db (@dbs) {
398 undef $self->{dbstat
}->{$db}->{dbh
};
399 unlink $self->{dbstat
}->{$db}->{tmpfilename
};
400 $self->{dbstat
}->{$db}->{dbh
} = $olddbh->{$db};
405 foreach my $db (@dbs) {
406 my $filename = $self->{dbstat
}->{$db}->{tmpfilename
};
407 $self->{dbstat
}->{$db}->{idcount
} = 0;
410 if ($db eq 'memberof') {
411 $self->{dbstat
}->{$db}->{dbh
} =
412 tie
(my %h, 'DB_File', $filename,
413 O_CREAT
|O_RDWR
, 0666, $DB_BTREE);
415 $self->{dbstat
}->{$db}->{dbh
} =
416 tie
(my %h, 'DB_File', $filename,
417 O_CREAT
|O_RDWR
, 0666, $DB_HASH);
420 die "unable to open database file '$filename': $!\n"
421 if !$self->{dbstat
}->{$db}->{dbh
};
426 $self->{errors
} .= $err;
431 $self->querygroups ($ldap) if $self->{groupbasedn
};
433 $self->queryusers($ldap) if !$self->{errors
};
437 if ($self->{errors
}) {
442 my $lock = lockdir
($self->{id
});
445 my $err = "unable to get database lock for ldap database '$self->{id}'";
446 $self->{errors
} .= "$err\n";
452 foreach my $db (@dbs) {
453 my $filename = $self->{dbstat
}->{$db}->{filename
} =
454 "$cachedir/$dir/${db}.db";
455 $self->{dbstat
}->{$db}->{dbh
}->sync(); # flush everything
456 rename $self->{dbstat
}->{$db}->{tmpfilename
}, $filename;
461 $last_atime->{$self->{id
}} = time();
463 $self->{gcount
} = $self->{dbstat
}->{groups
}->{idcount
};
464 $self->{ucount
} = __count_entries
($self->{dbstat
}->{accounts
}->{dbh
});
465 $self->{mcount
} = __count_entries
($self->{dbstat
}->{mails
}->{dbh
});
468 sub __count_entries
{
476 my $status = $dbh->seq($key, $value, R_FIRST
());
478 while ($status == 0) {
480 $status = $dbh->seq($key, $value, R_NEXT
());
487 my ($self, $try) = @_;
489 my $dir = "ldapdb_" . $self->{id
};
490 mkdir "$cachedir/$dir";
492 my $filename = "$cachedir/$dir/mails.db";
494 return if $last_atime->{$self->{id
}} &&
495 PMG
::Utils
::file_older_than
($filename, $last_atime->{$self->{id
}});
498 foreach my $db (@dbs) {
499 my $filename = $self->{dbstat
}->{$db}->{filename
} =
500 "$cachedir/$dir/${db}.db";
501 $self->{dbstat
}->{$db}->{idcount
} = 0;
502 if ($db eq 'memberof') {
503 $self->{dbstat
}->{$db}->{dbh
} =
504 tie
(my %h, 'DB_File', $filename,
505 O_RDONLY
, 0666, $DB_BTREE);
507 $self->{dbstat
}->{$db}->{dbh
} =
508 tie
(my %h, 'DB_File', $filename,
509 O_RDONLY
, 0666, $DB_HASH);
512 if (!$self->{dbstat
}->{$db}->{dbh
} && !$try) {
513 my $err = "ldap error - unable to open database file '$filename': $!";
514 $self->{errors
} .= "$err\n";
515 syslog
('err', $err) if !$self->{dbstat
}->{$db}->{dbh
};
520 $last_atime->{$self->{id
}} = time();
522 $self->{gcount
} = __count_entries
($self->{dbstat
}->{groups
}->{dbh
});
523 $self->{ucount
} = __count_entries
($self->{dbstat
}->{accounts
}->{dbh
});
524 $self->{mcount
} = __count_entries
($self->{dbstat
}->{mails
}->{dbh
});
528 my ($self, $force) = @_;
530 $self->{errors
} = '';
533 # only sync if file is older than 1 hour
535 my $dir = "ldapdb_" . $self->{id
};
536 mkdir "$cachedir/$dir";
537 my $filename = "$cachedir/$dir/mails.db";
540 !PMG
::Utils
::file_older_than
($filename, time() - 3600)) {
546 $self->sync_database();
548 if ($self->{errors
}) {
558 my $dbh = $self->{dbstat
}->{groups
}->{dbh
};
560 return $res if !$dbh;
564 my $status = $dbh->seq($key, $value, R_FIRST
());
567 while ($status == 0) {
571 $status = $dbh->seq($key, $value, R_NEXT
());
582 my $dbh = $self->{dbstat
}->{users
}->{dbh
};
584 return $res if !$dbh;
588 my $status = $dbh->seq($key, $value, R_FIRST
());
591 while ($status == 0) {
592 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $value);
598 $status = $dbh->seq($key, $value, R_NEXT
());
605 my ($self, $mail) = @_;
607 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
608 my $dbhusers = $self->{dbstat
}->{users
}->{dbh
};
610 return undef if !$dbhmails || !$dbhusers;
617 $dbhmails->get($mail, $cuid);
618 return undef if !$cuid;
621 $dbhusers->get($cuid, $rdata);
622 return undef if !$rdata;
624 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
626 push @$res, { primary
=> 1, email
=> $pmail };
630 my $status = $dbhmails->seq($key, $value, R_FIRST
());
632 while ($status == 0) {
633 if ($value == $cuid && $key ne $pmail) {
634 push @$res, { primary
=> 0, email
=> $key };
636 $status = $dbhmails->seq($key, $value, R_NEXT
());
643 my ($self, $mail) = @_;
645 my $dbh = $self->{dbstat
}->{mails
}->{dbh
};
651 $dbh->get($mail, $res);
656 my ($self, $account) = @_;
658 my $dbh = $self->{dbstat
}->{accounts
}->{dbh
};
661 $account = lc($account);
664 $dbh->get($account, $res);
669 my ($self, $group) = @_;
671 my $dbh = $self->{dbstat
}->{groups
}->{dbh
};
675 $dbh->get($group, $res);
679 sub account_has_address
{
680 my ($self, $account, $mail) = @_;
682 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
683 my $dbhaccounts = $self->{dbstat
}->{accounts
}->{dbh
};
684 return 0 if !$dbhmails || !$dbhaccounts;
686 $account = lc($account);
690 $dbhaccounts->get($account, $accid);
694 $dbhmails->get($mail, $mailid);
695 return 0 if !$mailid;
697 return ($accid == $mailid);
701 my ($self, $mail, $group) = @_;
703 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
704 my $dbhgroups = $self->{dbstat
}->{groups
}->{dbh
};
705 my $dbhmemberof = $self->{dbstat
}->{memberof
}->{dbh
};
707 return 0 if !$dbhmails || !$dbhgroups || !$dbhmemberof;
712 $dbhmails->get($mail, $cuid);
716 $dbhgroups->get($group, $groupid);
717 return 0 if !$groupid;
719 my @gida = $dbhmemberof->get_dup($cuid);
721 return grep { $_ eq $groupid } @gida;
725 my ($self, $mail, $scan) = @_;
727 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
728 my $dbhusers = $self->{dbstat
}->{users
}->{dbh
};
730 return undef if !$dbhmails || !$dbhusers;
737 $dbhmails->get($mail, $cuid);
738 return undef if !$cuid;
741 $dbhusers->get($cuid, $rdata);
742 return undef if !$rdata;
744 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
747 $res->{account
} = $account;
748 $res->{pmail
} = $pmail;
753 my $status = $dbhmails->seq($key, $value, R_FIRST
());
756 while ($status == 0) {
757 push @$mails, $key if $value == $cuid;
758 $status = $dbhmails->seq($key, $value, R_NEXT
());
760 $res->{mails
} = $mails;