]>
git.proxmox.com Git - pmg-api.git/blob - src/PMG/LDAPCache.pm
1 package PMG
::LDAPCache
;
11 use PVE
::Tools
qw(split_list);
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 my $config_properties = PMG
::LDAPConfig
::properties
();
56 # set defaults for the fields that have one
57 foreach my $property (keys %$config_properties) {
58 my $d = $config_properties->{$property};
59 next if !defined($d->{default});
60 $self->{$property} = $args{$property} || $d->{default};
63 # split list returns an array not a reference
64 $self->{accountattr
} = [split_list
($self->{accountattr
})];
65 $self->{mailattr
} = [split_list
($self->{mailattr
})];
66 $self->{groupclass
} = [split_list
($self->{groupclass
})];
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
};
76 $self->{verify
} = $args{verify
};
77 $self->{cafile
} = $args{cafile
};
79 if ($args{syncmode
} == 1) {
80 # read local data only
86 return $self if !($args{server1
});
88 if ($args{syncmode
} == 2) {
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);
110 my ($class, $id) = @_;
112 if (my $lock = lockdir
($id)) {
113 delete $ldapcache->{$id};
114 delete $last_atime->{$id};
115 my $dir = "$cachedir/ldapdb_$id";
119 syslog
('err' , "can't lock ldap database '$id'");
124 my ($self, $syncmode) = @_;
126 if ($syncmode == 1) {
127 # read local data only
128 $self->{errors
} = '';
130 } elsif ($syncmode == 2) {
139 my ($self, $ldap) = @_;
142 my $attrs = [ @{$self->{mailattr
}}, @{$self->{accountattr
}}, 'memberOf' ];
145 my $users = eval { PVE
::LDAP
::query_users
($ldap, $self->{filter
}, $attrs, $self->{basedn
}) };
147 $self->{errors
} .= "$err\n";
152 foreach my $user (@$users) {
153 my $dn = $user->{dn
};
158 foreach my $attr (@{$self->{mailattr
}}) {
159 next if !$user->{attributes
}->{$attr};
160 foreach my $mail (@{$user->{attributes
}->{$attr}}) {
162 # Test if the Line starts with one of the following lines:
163 # proxyAddresses: [smtp|SMTP]:
164 # and also discard this starting string, so that $mail is only the
165 # address without any other characters...
167 $mail =~ s/^(smtp|SMTP)[\:\$]//gs;
169 if ($mail !~ m/[\{\}\\\/]/ && $mail =~ m/^\S
+\
@\S+$/) {
170 $umails->{$mail} = 1;
171 $pmail = $mail if !$pmail;
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} = $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);
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;
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
=> $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
=> $key };
623 $status = $dbhmails->seq($key, $value, R_NEXT
());
630 my ($self, $mail) = @_;
632 my $dbh = $self->{dbstat
}->{mails
}->{dbh
};
638 $dbh->get($mail, $res);
643 my ($self, $account) = @_;
645 my $dbh = $self->{dbstat
}->{accounts
}->{dbh
};
648 $account = lc($account);
651 $dbh->get($account, $res);
656 my ($self, $group) = @_;
658 my $dbh = $self->{dbstat
}->{groups
}->{dbh
};
662 $dbh->get($group, $res);
666 sub account_has_address
{
667 my ($self, $account, $mail) = @_;
669 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
670 my $dbhaccounts = $self->{dbstat
}->{accounts
}->{dbh
};
671 return 0 if !$dbhmails || !$dbhaccounts;
673 $account = lc($account);
677 $dbhaccounts->get($account, $accid);
681 $dbhmails->get($mail, $mailid);
682 return 0 if !$mailid;
684 return ($accid == $mailid);
688 my ($self, $mail, $group) = @_;
690 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
691 my $dbhgroups = $self->{dbstat
}->{groups
}->{dbh
};
692 my $dbhmemberof = $self->{dbstat
}->{memberof
}->{dbh
};
694 return 0 if !$dbhmails || !$dbhgroups || !$dbhmemberof;
699 $dbhmails->get($mail, $cuid);
703 $dbhgroups->get($group, $groupid);
704 return 0 if !$groupid;
706 my @gida = $dbhmemberof->get_dup($cuid);
708 return grep { $_ eq $groupid } @gida;
712 my ($self, $mail, $scan) = @_;
714 my $dbhmails = $self->{dbstat
}->{mails
}->{dbh
};
715 my $dbhusers = $self->{dbstat
}->{users
}->{dbh
};
717 return undef if !$dbhmails || !$dbhusers;
724 $dbhmails->get($mail, $cuid);
725 return undef if !$cuid;
728 $dbhusers->get($cuid, $rdata);
729 return undef if !$rdata;
731 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
734 $res->{account
} = $account;
735 $res->{pmail
} = $pmail;
740 my $status = $dbhmails->seq($key, $value, R_FIRST
());
743 while ($status == 0) {
744 push @$mails, $key if $value == $cuid;
745 $status = $dbhmails->seq($key, $value, R_NEXT
());
747 $res->{mails
} = $mails;