]>
git.proxmox.com Git - pmg-api.git/blob - src/PMG/LDAPCache.pm
1 package PMG
::LDAPCache
;
9 use Encode
qw(encode decode);
12 use PVE
::Tools
qw(split_list);
18 $DB_HASH->{'cachesize'} = 10000;
19 $DB_RECNO->{'cachesize'} = 10000;
20 $DB_BTREE->{'cachesize'} = 10000;
21 $DB_BTREE->{'flags'} = R_DUP
;
23 my $cachedir = '/var/lib/pmg';
30 # users (hash): UID -> pmail, account, DN
31 # dnames (hash): DN -> UID
32 # accounts (hash): account -> UID
33 # mail (hash): mail -> UID
34 # groups (hash): group -> GID
35 # memberof (btree): UID -> GID
37 my @dbs = ('users', 'dnames', 'groups', 'mails', 'accounts', 'memberof');
40 my ($self, %args) = @_;
42 my $type = ref($self) || $self;
44 die "undefined ldap id" if !$args{id
};
48 if ($ldapcache->{$id}) {
49 $self = $ldapcache->{$id};
51 $ldapcache->{$id} = $self = bless {}, $type;
55 my $config_properties = PMG
::LDAPConfig
::properties
();
57 # set defaults for the fields that have one
58 foreach my $property (keys %$config_properties) {
59 my $d = $config_properties->{$property};
60 next if !defined($d->{default});
61 $self->{$property} = $args{$property} || $d->{default};
64 # split list returns an array not a reference
65 $self->{accountattr
} = [split_list
($self->{accountattr
})];
66 $self->{mailattr
} = [split_list
($self->{mailattr
})];
67 $self->{groupclass
} = [split_list
($self->{groupclass
})];
69 $self->{server1
} = $args{server1
};
70 $self->{server2
} = $args{server2
};
71 $self->{binddn
} = $args{binddn
};
72 $self->{bindpw
} = $args{bindpw
};
73 $self->{basedn
} = $args{basedn
};
74 $self->{port
} = $args{port
};
75 $self->{groupbasedn
} = $args{groupbasedn
};
76 $self->{filter
} = $args{filter
};
77 $self->{verify
} = $args{verify
};
78 $self->{cafile
} = $args{cafile
};
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) = @_;
113 if (my $lock = lockdir
($id)) {
114 delete $ldapcache->{$id};
115 delete $last_atime->{$id};
116 my $dir = "$cachedir/ldapdb_$id";
120 syslog
('err' , "can't lock ldap database '$id'");
125 my ($self, $syncmode) = @_;
127 if ($syncmode == 1) {
128 # read local data only
129 $self->{errors
} = '';
131 } elsif ($syncmode == 2) {
140 my ($self, $ldap) = @_;
143 my $attrs = [ @{$self->{mailattr
}}, @{$self->{accountattr
}}, 'memberOf' ];
146 my $users = eval { PVE
::LDAP
::query_users
($ldap, $self->{filter
}, $attrs, $self->{basedn
}) };
148 $self->{errors
} .= "$err\n";
153 foreach my $user (@$users) {
154 my $dn = $user->{dn
};
159 foreach my $attr (@{$self->{mailattr
}}) {
160 next if !$user->{attributes
}->{$attr};
161 foreach my $mail (@{$user->{attributes
}->{$attr}}) {
163 # Test if the Line starts with `proxyAddresses: [smtp]:`, discard this starting
164 # string, so that $mail is only the plain address without any extra characters
165 $mail =~ s/^smtp[\:\$]//gs;
167 next if $mail =~ m/[\{\}\\\/]/ || $mail !~ m/^\S
+\
@\S+$/;
168 # exclude sip and x500 addresses in proxyAddresses http://archive.today/XIerB
169 next if $mail =~ m/^(sip|x500)[\:\$]/;
171 $umails->{$mail} = 1;
172 $pmail = $mail if !$pmail; # use first one as primary mail
175 my $addresses = [ keys %$umails ];
177 next if !$pmail; # account has no email addresses
180 $self->{dbstat
}->{dnames
}->{dbh
}->get($dn, $cuid);
182 $cuid = ++$self->{dbstat
}->{dnames
}->{idcount
};
183 $self->{dbstat
}->{dnames
}->{dbh
}->put($dn, $cuid);
186 foreach my $attr (@{$self->{accountattr
}}) {
187 next if !$user->{attributes
}->{$attr};
188 foreach my $account (@{$user->{attributes
}->{$attr}}) {
189 next if !defined($account) || !length($account);
191 $account = lc($account);
192 $self->{dbstat
}->{accounts
}->{dbh
}->put($account, $cuid);
193 my $data = pack('n/a* n/a* n/a*', $pmail, $account, $dn);
194 $self->{dbstat
}->{users
}->{dbh
}->put($cuid, $data);
198 foreach my $mail (@$addresses) {
199 $self->{dbstat
}->{mails
}->{dbh
}->put($mail, $cuid);
202 if (!$self->{groupbasedn
}) {
203 foreach my $group (@{$user->{groups
}}) {
205 $self->{dbstat
}->{groups
}->{dbh
}->get($group, $cgid);
207 $cgid = ++$self->{dbstat
}->{groups
}->{idcount
};
208 $self->{dbstat
}->{groups
}->{dbh
}->put($group, $cgid);
210 $self->{dbstat
}->{memberof
}->{dbh
}->put($cuid, $cgid);
217 my ($self, $ldap) = @_;
219 return undef if !$self->{groupbasedn
};
221 my $groups = eval { PVE
::LDAP
::query_groups
($ldap, $self->{groupbasedn
}, $self->{groupclass
}) };
223 $self->{errors
} .= "$err\n";
228 foreach my $group (@$groups) {
229 my $dn = $group->{dn
};
232 $self->{dbstat
}->{groups
}->{dbh
}->get($dn, $cgid);
234 $cgid = ++$self->{dbstat
}->{groups
}->{idcount
};
235 $self->{dbstat
}->{groups
}->{dbh
}->put($dn, $cgid);
238 foreach my $m (@{$group->{members
}}) {
240 $self->{dbstat
}->{dnames
}->{dbh
}->get($m, $cuid);
242 $cuid = ++$self->{dbstat
}->{dnames
}->{idcount
};
243 $self->{dbstat
}->{dnames
}->{dbh
}->put($m, $cuid);
246 $self->{dbstat
}->{memberof
}->{dbh
}->put($cuid, $cgid);
254 my $hosts = [ $self->{server1
} ];
255 push @$hosts, $self->{server2
} if $self->{server2
};
258 my $scheme = $self->{mode
};
260 if ($scheme eq 'ldaps' || $scheme eq 'ldap+starttls') {
261 if ($self->{verify
}) {
262 $opts->{verify
} = 'require';
263 } elsif ($scheme eq 'ldap+starttls') {
264 $opts->{verify
} = 'none';
267 if ($self->{cafile
}) {
268 $opts->{cafile
} = $self->{cafile
};
270 $opts->{capath
} = '/etc/ssl/certs/';
274 return PVE
::LDAP
::ldap_connect
($hosts, $scheme, $self->{port
}, $opts);
277 sub ldap_connect_and_bind
{
280 my $ldap = eval { $self->ldap_connect() };
281 die "Can't bind to ldap server '$self->{id}': " . ($@) . "\n" if $@;
285 $dn = $self->{binddn
} if $self->{binddn
};
286 $pw = $self->{bindpw
} if $self->{bindpw
};
287 PVE
::LDAP
::ldap_bind
($ldap, $dn, $pw);
289 if (!$self->{basedn
}) {
290 my $root = $ldap->root_dse(attrs
=> [ 'defaultNamingContext' ]);
291 $self->{basedn
} = $root->get_value('defaultNamingContext');
300 my $dir = "ldapdb_" . $self->{id
};
301 mkdir "$cachedir/$dir";
303 # open ldap connection
307 eval { $ldap = $self->ldap_connect_and_bind(); };
309 $self->{errors
} .= "$err\n";
314 # open temporary database files
318 foreach my $db (@dbs) {
319 $self->{dbstat
}->{$db}->{tmpfilename
} = "$cachedir/$dir/${db}_tmp$$.db";
320 $olddbh->{$db} = $self->{dbstat
}->{$db}->{dbh
};
323 my $error_cleanup = sub {
324 # close and delete all files
325 foreach my $db (@dbs) {
326 undef $self->{dbstat
}->{$db}->{dbh
};
327 unlink $self->{dbstat
}->{$db}->{tmpfilename
};
328 $self->{dbstat
}->{$db}->{dbh
} = $olddbh->{$db};
333 foreach my $db (@dbs) {
334 my $filename = $self->{dbstat
}->{$db}->{tmpfilename
};
335 $self->{dbstat
}->{$db}->{idcount
} = 0;
338 if ($db eq 'memberof') {
339 $self->{dbstat
}->{$db}->{dbh
} =
340 tie
(my %h, 'DB_File', $filename,
341 O_CREAT
|O_RDWR
, 0666, $DB_BTREE);
343 $self->{dbstat
}->{$db}->{dbh
} =
344 tie
(my %h, 'DB_File', $filename,
345 O_CREAT
|O_RDWR
, 0666, $DB_HASH);
348 die "unable to open database file '$filename': $!\n"
349 if !$self->{dbstat
}->{$db}->{dbh
};
354 $self->{errors
} .= $err;
359 $self->querygroups ($ldap) if $self->{groupbasedn
};
361 $self->queryusers($ldap) if !$self->{errors
};
365 if ($self->{errors
}) {
370 my $lock = lockdir
($self->{id
});
373 my $err = "unable to get database lock for ldap database '$self->{id}'";
374 $self->{errors
} .= "$err\n";
380 foreach my $db (@dbs) {
381 my $filename = $self->{dbstat
}->{$db}->{filename
} =
382 "$cachedir/$dir/${db}.db";
383 $self->{dbstat
}->{$db}->{dbh
}->sync(); # flush everything
384 rename $self->{dbstat
}->{$db}->{tmpfilename
}, $filename;
389 $last_atime->{$self->{id
}} = time();
391 $self->{gcount
} = $self->{dbstat
}->{groups
}->{idcount
};
392 $self->{ucount
} = __count_entries
($self->{dbstat
}->{accounts
}->{dbh
});
393 $self->{mcount
} = __count_entries
($self->{dbstat
}->{mails
}->{dbh
});
396 sub __count_entries
{
404 my $status = $dbh->seq($key, $value, R_FIRST
());
406 while ($status == 0) {
408 $status = $dbh->seq($key, $value, R_NEXT
());
415 my ($self, $try) = @_;
417 my $dir = "ldapdb_" . $self->{id
};
418 mkdir "$cachedir/$dir";
420 my $filename = "$cachedir/$dir/mails.db";
422 return if $last_atime->{$self->{id
}} &&
423 PMG
::Utils
::file_older_than
($filename, $last_atime->{$self->{id
}});
426 foreach my $db (@dbs) {
427 my $filename = $self->{dbstat
}->{$db}->{filename
} =
428 "$cachedir/$dir/${db}.db";
429 $self->{dbstat
}->{$db}->{idcount
} = 0;
430 if ($db eq 'memberof') {
431 $self->{dbstat
}->{$db}->{dbh
} =
432 tie
(my %h, 'DB_File', $filename,
433 O_RDONLY
, 0666, $DB_BTREE);
435 $self->{dbstat
}->{$db}->{dbh
} =
436 tie
(my %h, 'DB_File', $filename,
437 O_RDONLY
, 0666, $DB_HASH);
440 if (!$self->{dbstat
}->{$db}->{dbh
} && !$try) {
441 my $err = "ldap error - unable to open database file '$filename': $!";
442 $self->{errors
} .= "$err\n";
443 syslog
('err', $err) if !$self->{dbstat
}->{$db}->{dbh
};
448 $last_atime->{$self->{id
}} = time();
450 $self->{gcount
} = __count_entries
($self->{dbstat
}->{groups
}->{dbh
});
451 $self->{ucount
} = __count_entries
($self->{dbstat
}->{accounts
}->{dbh
});
452 $self->{mcount
} = __count_entries
($self->{dbstat
}->{mails
}->{dbh
});
456 my ($self, $force) = @_;
458 $self->{errors
} = '';
461 # only sync if file is older than 1 hour
463 my $dir = "ldapdb_" . $self->{id
};
464 mkdir "$cachedir/$dir";
465 my $filename = "$cachedir/$dir/mails.db";
468 !PMG
::Utils
::file_older_than
($filename, time() - 3600)) {
474 $self->sync_database();
476 if ($self->{errors
}) {
486 my $dbh = $self->{dbstat
}->{groups
}->{dbh
};
488 return $res if !$dbh;
492 my $status = $dbh->seq($key, $value, R_FIRST
());
494 while ($status == 0) {
495 $res->{$value} = PMG
::Utils
::try_decode_utf8
($key);
496 $status = $dbh->seq($key, $value, R_NEXT
());
507 my $dbh = $self->{dbstat
}->{users
}->{dbh
};
509 return $res if !$dbh;
513 my $status = $dbh->seq($key, $value, R_FIRST
());
516 while ($status == 0) {
517 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $value);
519 pmail
=> PMG
::Utils
::try_decode_utf8
($pmail),
520 account
=> PMG
::Utils
::try_decode_utf8
($account),
521 dn
=> PMG
::Utils
::try_decode_utf8
($dn),
523 $status = $dbh->seq($key, $value, R_NEXT
());
529 sub get_gid_uid_map
{
532 my $dbh = $self->{dbstat
}->{memberof
}->{dbh
};
541 if($dbh->seq($key, $value, R_FIRST
()) == 0) {
543 push @{$map->{$value}}, $key;
544 } while($dbh->seq($key, $value, R_NEXT
()) == 0);
555 my $groups = $self->get_groups();
557 for my $gid (sort keys %$groups) {
559 dn
=> $groups->{$gid},
568 my ($self, $gid) = @_;
572 my $users = $self->get_users();
574 if (!defined($gid)) {
575 $res = [values %$users];
577 my $gid_uid_map = $self->get_gid_uid_map();
578 my $groups = $self->get_groups();
579 die "No such Group ID\n"
580 if !defined($groups->{$gid});
581 my $memberuids = $gid_uid_map->{$gid};
582 for my $uid (@$memberuids) {
583 next if !defined($users->{$uid});
584 push @$res, $users->{$uid};
592 my ($self, $mail) = @_;
594 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
595 my $dbhusers = $self->{dbstat
}->{users
}->{dbh
};
597 return undef if !$dbhmails || !$dbhusers;
599 $mail = encode
('UTF-8', lc($mail));
604 $dbhmails->get($mail, $cuid);
605 return undef if !$cuid;
608 $dbhusers->get($cuid, $rdata);
609 return undef if !$rdata;
611 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
613 push @$res, { primary
=> 1, email
=> PMG
::Utils
::try_decode_utf8
($pmail) };
617 my $status = $dbhmails->seq($key, $value, R_FIRST
());
619 while ($status == 0) {
620 if ($value == $cuid && $key ne $pmail) {
621 push @$res, { primary
=> 0, email
=> PMG
::Utils
::try_decode_utf8
($key) };
623 $status = $dbhmails->seq($key, $value, R_NEXT
());
630 my ($self, $mail) = @_;
632 my $dbh = $self->{dbstat
}->{mails
}->{dbh
};
635 $mail = encode
('UTF-8', lc($mail));
638 $dbh->get($mail, $res);
643 my ($self, $account) = @_;
645 my $dbh = $self->{dbstat
}->{accounts
}->{dbh
};
648 $account = encode
('UTF-8', lc($account));
651 $dbh->get($account, $res);
656 my ($self, $group) = @_;
658 my $dbh = $self->{dbstat
}->{groups
}->{dbh
};
661 $group = encode
('UTF-8', $group);
664 $dbh->get($group, $res);
668 sub account_has_address
{
669 my ($self, $account, $mail) = @_;
671 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
672 my $dbhaccounts = $self->{dbstat
}->{accounts
}->{dbh
};
673 return 0 if !$dbhmails || !$dbhaccounts;
675 $account = encode
('UTF-8', lc($account));
676 $mail = encode
('UTF-8', lc($mail));
679 $dbhaccounts->get($account, $accid);
683 $dbhmails->get($mail, $mailid);
684 return 0 if !$mailid;
686 return ($accid == $mailid);
690 my ($self, $mail, $group) = @_;
692 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
693 my $dbhgroups = $self->{dbstat
}->{groups
}->{dbh
};
694 my $dbhmemberof = $self->{dbstat
}->{memberof
}->{dbh
};
696 return 0 if !$dbhmails || !$dbhgroups || !$dbhmemberof;
698 $mail = encode
('UTF-8', lc($mail));
701 $dbhmails->get($mail, $cuid);
704 $group = encode
('UTF-8', $group);
707 $dbhgroups->get($group, $groupid);
708 return 0 if !$groupid;
710 my @gida = $dbhmemberof->get_dup($cuid);
712 return grep { $_ eq $groupid } @gida;
716 my ($self, $mail, $scan) = @_;
718 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
719 my $dbhusers = $self->{dbstat
}->{users
}->{dbh
};
721 return undef if !$dbhmails || !$dbhusers;
723 $mail = encode
('UTF-8', lc($mail));
728 $dbhmails->get($mail, $cuid);
729 return undef if !$cuid;
732 $dbhusers->get($cuid, $rdata);
733 return undef if !$rdata;
735 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
738 $res->{account
} = $account;
739 $res->{pmail
} = $pmail;
744 my $status = $dbhmails->seq($key, $value, R_FIRST
());
747 while ($status == 0) {
748 push @$mails, $key if $value == $cuid;
749 $status = $dbhmails->seq($key, $value, R_NEXT
());
751 $res->{mails
} = $mails;