use strict;
use warnings;
-use Carp;
use File::Path;
use LockFile::Simple;
+use Data::Dumper;
use Net::LDAP;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw (LDAP_CONTROL_PAGED);
use DB_File;
use PVE::SafeSyslog;
+use PVE::Tools qw(split_list);
use PMG::Utils;
+use PMG::LDAPConfig;
$DB_HASH->{'cachesize'} = 10000;
$DB_RECNO->{'cachesize'} = 10000;
$DB_BTREE->{'cachesize'} = 10000;
$DB_BTREE->{'flags'} = R_DUP ;
-my $cachedir = '/var/lib/proxmox-mailgateway';
+my $cachedir = '/var/lib/pmg';
my $last_atime = {};
my $ldapcache = {};
$self->{id} = $id;
}
- if (!$args{mailattr}) {
- $args{mailattr} = "mail, userPrincipalName, proxyAddresses, othermailbox";
- }
- $args{mailattr} =~ s/[\,\;]/ /g;
- $args{mailattr} =~ s/\s+/,/g;
+ my $config_properties = PMG::LDAPConfig::properties();
- if ($args{mode} && ($args{mode} eq 'ldap' || $args{mode} eq 'ldaps')) {
- $self->{mode} = $args{mode};
- } else {
- $self->{mode} = 'ldap';
+ # set defaults for the fields that have one
+ foreach my $property (keys %$config_properties) {
+ my $d = $config_properties->{$property};
+ next if !defined($d->{default});
+ $self->{$property} = $args{$property} || $d->{default};
}
- $self->{accountattr} = $args{accountattr} || 'sAMAccountName';
- @{$self->{mailattr}} = split(/,/, $args{mailattr});
+ # split list returns an array not a reference
+ $self->{accountattr} = [split_list($self->{accountattr})];
+ $self->{mailattr} = [split_list($self->{mailattr})];
+ $self->{groupclass} = [split_list($self->{groupclass})];
+
$self->{server1} = $args{server1};
$self->{server2} = $args{server2};
$self->{binddn} = $args{binddn};
scope => "subtree",
filter => $filter,
control => [ $page ],
- attrs => [ @{$self->{mailattr}}, $self->{accountattr}, 'memberOf' ]
+ attrs => [ @{$self->{mailattr}}, @{$self->{accountattr}}, 'memberOf' ]
);
my $cookie;
$self->{dbstat}->{dnames}->{dbh}->put($dn, $cuid);
}
- my $account = $entry->get_value($self->{accountattr});
- if ($account && ($account =~ m/^\S+$/s)) {
- $account = lc($account);
- $self->{dbstat}->{accounts}->{dbh}->put($account, $cuid);
- } else {
- $account = '';
+ foreach my $attr (@{$self->{accountattr}}) {
+ my $account = $entry->get_value($attr);
+ if ($account && ($account =~ m/^\S+$/s)) {
+ $account = lc($account);
+ $self->{dbstat}->{accounts}->{dbh}->put($account, $cuid);
+ my $data = pack('n/a* n/a* n/a*', $pmail, $account, $dn);
+ $self->{dbstat}->{users}->{dbh}->put($cuid, $data);
+ }
}
- my $data = pack('n/a* n/a* n/a*', $pmail, $account, $dn);
- $self->{dbstat}->{users}->{dbh}->put($cuid, $data);
-
foreach my $mail (@$addresses) {
$self->{dbstat}->{mails}->{dbh}->put($mail, $cuid);
}
# Get cookie from paged control
my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
- $cookie = $resp->cookie or last;
+ $cookie = $resp->cookie;
+
+ last if (!defined($cookie) || !length($cookie));
# Set cookie in paged control
$page->cookie($cookie);
}
- if ($cookie) {
+ if (defined($cookie) && length($cookie)) {
# We had an abnormal exit, so let the server know we do not want any more
$page->cookie($cookie);
$page->size(0);
return undef if !$self->{groupbasedn};
- my $filter = "(objectclass=group)";
+ my $filter = "(|";
+
+ for my $class (@{$self->{groupclass}}) {
+ $filter .= "(objectclass=$class)";
+ }
+
+ $filter .= ")";
my $page = Net::LDAP::Control::Paged->new(size => 100);
scope => "subtree",
filter => $filter,
control => [ $page ],
- attrs => [ 'member' ]
+ attrs => [ 'member', 'uniqueMember' ],
);
my $cookie;
foreach my $entry ( $mesg->entries ) {
my $group = $entry->dn;
my @members = $entry->get_value('member');
-
+ if (!scalar(@members)) {
+ @members = $entry->get_value('uniqueMember');
+ }
my $cgid;
$self->{dbstat}->{groups}->{dbh}->get($group, $cgid);
if (!$cgid) {
sub ldap_connect {
my ($self) = @_;
- my $mode = $self->{mode};
- my $portstr = '';
- $portstr = ':' . $self->{port} if $self->{port};
+ my $hosts = [ $self->{server1} ];
- my $serverstr = "$mode://$self->{server1}${portstr}/";
- my $ldap = Net::LDAP->new($serverstr);
- if(!$ldap && $self->{server2} && $self->{server2} ne '127.0.0.1') {
- $serverstr = "$mode://$self->{server2}${portstr}/";
- $ldap = Net::LDAP->new($serverstr);
- }
+ push @$hosts, $self->{server2} if $self->{server2};
+
+ my $opts = { timeout => 10, onerror => 'die' };
+
+ $opts->{port} = $self->{port} if $self->{port};
+ $opts->{schema} = $self->{mode};
- return $ldap;
+ return Net::LDAP->new($hosts, %$opts);
+}
+
+sub ldap_connect_and_bind {
+ my ($self) = @_;
+
+ my $ldap = $self->ldap_connect() ||
+ die "Can't bind to ldap server '$self->{id}': $! $@\n";
+
+ my $mesg;
+
+ if ($self->{binddn}) {
+ $mesg = $ldap->bind($self->{binddn}, password => $self->{bindpw});
+ } else {
+ $mesg = $ldap->bind(); # anonymous bind
+ }
+
+ die "ldap bind failed: " . $mesg->error . "\n" if $mesg->code;
+
+ if (!$self->{basedn}) {
+ my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]);
+ $self->{basedn} = $root->get_value('defaultNamingContext');
+ }
+
+ return $ldap;
}
sub sync_database {
# open ldap connection
- syslog('info', "syncing ldap database '$self->{id}'");
+ my $ldap;
- my $ldap = $self->ldap_connect();
-
- if (!$ldap) {
- my $err = "Can't bind to ldap server '$self->{id}': $!";
+ eval { $ldap = $self->ldap_connect_and_bind(); };
+ if (my $err = $@) {
$self->{errors} .= "$err\n";
syslog('err', $err);
return;
}
- my $mesg;
-
- if ($self->{binddn}) {
- $mesg = $ldap->bind($self->{binddn}, password => $self->{bindpw});
- } else {
- $mesg = $ldap->bind(); # anonymous bind
- }
-
- if ($mesg->code) {
- my $err = "ldap bind failed: " . $mesg->error;
- $self->{errors} .= "$err\n";
- syslog('err', $err);
- return;
- }
-
- if (!$self->{basedn}) {
- my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]);
- $self->{basedn} = $root->get_value('defaultNamingContext');
- }
-
# open temporary database files
my $olddbh = {};
$olddbh->{$db} = $self->{dbstat}->{$db}->{dbh};
}
+ my $error_cleanup = sub {
+ # close and delete all files
+ foreach my $db (@dbs) {
+ undef $self->{dbstat}->{$db}->{dbh};
+ unlink $self->{dbstat}->{$db}->{tmpfilename};
+ $self->{dbstat}->{$db}->{dbh} = $olddbh->{$db};
+ }
+ };
+
eval {
foreach my $db (@dbs) {
my $filename = $self->{dbstat}->{$db}->{tmpfilename};
if !$self->{dbstat}->{$db}->{dbh};
}
};
-
- my $err = $@;
-
- if ($err) {
- # close and delete all files
- foreach my $db (@dbs) {
- undef $self->{dbstat}->{$db}->{dbh};
- unlink $self->{dbstat}->{$db}->{tmpfilename};
- $self->{dbstat}->{$db}->{dbh} = $olddbh->{$db};
- }
+ if (my $err = $@) {
+ $error_cleanup->();
$self->{errors} .= $err;
syslog('err', $err);
-
return;
}
$self->querygroups ($ldap) if $self->{groupbasedn};
- if (!$self->{errors}) {
- $self->queryusers($ldap);
- }
+ $self->queryusers($ldap) if !$self->{errors};
$ldap->unbind;
if ($self->{errors}) {
- # close and delete all files
- foreach my $db (@dbs) {
- undef $self->{dbstat}->{$db}->{dbh};
- unlink $self->{dbstat}->{$db}->{tmpfilename};
- $self->{dbstat}->{$db}->{dbh} = $olddbh->{$db};
- }
- } else {
+ $error_cleanup->();
+ return;
+ }
- my $lock = lockdir($self->{id});
+ my $lock = lockdir($self->{id});
- if (!$lock) {
- my $err = "unable to get database lock for ldap database '$self->{id}'";
- $self->{errors} .= "$err\n";
- syslog('err', $err);
+ if (!$lock) {
+ my $err = "unable to get database lock for ldap database '$self->{id}'";
+ $self->{errors} .= "$err\n";
+ syslog('err', $err);
+ $error_cleanup->();
+ return;
+ }
- # close and delete all files
- foreach my $db (@dbs) {
- undef $self->{dbstat}->{$db}->{dbh};
- unlink $self->{dbstat}->{$db}->{tmpfilename};
- $self->{dbstat}->{$db}->{dbh} = $olddbh->{$db};
- }
- } else {
- foreach my $db (@dbs) {
- my $filename = $self->{dbstat}->{$db}->{filename} =
- "$cachedir/$dir/${db}.db";
- $self->{dbstat}->{$db}->{dbh}->sync(); # flush everything
- rename $self->{dbstat}->{$db}->{tmpfilename}, $filename;
- }
+ foreach my $db (@dbs) {
+ my $filename = $self->{dbstat}->{$db}->{filename} =
+ "$cachedir/$dir/${db}.db";
+ $self->{dbstat}->{$db}->{dbh}->sync(); # flush everything
+ rename $self->{dbstat}->{$db}->{tmpfilename}, $filename;
+ }
- $lock->release;
+ $lock->release;
- $last_atime->{$self->{id}} = time();
+ $last_atime->{$self->{id}} = time();
- $self->{gcount} = $self->{dbstat}->{groups}->{idcount};
- $self->{ucount} = __count_entries($self->{dbstat}->{accounts}->{dbh});
- $self->{mcount} = __count_entries($self->{dbstat}->{mails}->{dbh});
- }
- }
+ $self->{gcount} = $self->{dbstat}->{groups}->{idcount};
+ $self->{ucount} = __count_entries($self->{dbstat}->{accounts}->{dbh});
+ $self->{mcount} = __count_entries($self->{dbstat}->{mails}->{dbh});
}
sub __count_entries {
}
}
-sub groups {
+sub get_groups {
my ($self) = @_;
+ my $res = {};
+
my $dbh = $self->{dbstat}->{groups}->{dbh};
- return [] if !$dbh;
+
+ return $res if !$dbh;
+
+ my $key = 0 ;
+ my $value = "" ;
+ my $status = $dbh->seq($key, $value, R_FIRST());
+
+ while ($status == 0) {
+ $res->{$value} = $key;
+ $status = $dbh->seq($key, $value, R_NEXT());
+ }
+
+ return $res;
+}
+
+sub get_users {
+ my ($self) = @_;
+
+ my $res = {};
+
+ my $dbh = $self->{dbstat}->{users}->{dbh};
+
+ return $res if !$dbh;
my $key = 0 ;
my $value = "" ;
my $keys;
while ($status == 0) {
- push @$keys, $key;
+ my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $value);
+ $res->{$key} = {
+ pmail => $pmail,
+ account => $account,
+ dn => $dn,
+ };
$status = $dbh->seq($key, $value, R_NEXT());
}
- return $keys;
+ return $res;
+}
+
+sub get_gid_uid_map {
+ my ($self) = @_;
+
+ my $dbh = $self->{dbstat}->{memberof}->{dbh};
+
+ return [] if !$dbh;
+
+ my $key = 0 ;
+ my $value = "" ;
+
+ my $map = {};
+
+ if($dbh->seq($key, $value, R_FIRST()) == 0) {
+ do {
+ push @{$map->{$value}}, $key;
+ } while($dbh->seq($key, $value, R_NEXT()) == 0);
+ }
+
+ return $map;
+}
+
+sub list_groups {
+ my ($self) = @_;
+
+ my $res = [];
+
+ my $groups = $self->get_groups();
+
+ for my $gid (sort keys %$groups) {
+ push @$res, {
+ dn => $groups->{$gid},
+ gid => $gid,
+ };
+ }
+
+ return $res;
+}
+
+sub list_users {
+ my ($self, $gid) = @_;
+
+ my $res = [];
+
+ my $users = $self->get_users();
+
+ if (!defined($gid)) {
+ $res = [values %$users];
+ } else {
+ my $gid_uid_map = $self->get_gid_uid_map();
+ my $groups = $self->get_groups();
+ die "No such Group ID\n"
+ if !defined($groups->{$gid});
+ my $memberuids = $gid_uid_map->{$gid};
+ for my $uid (@$memberuids) {
+ next if !defined($users->{$uid});
+ push @$res, $users->{$uid};
+ }
+ }
+
+ return $res;
+}
+
+sub list_addresses {
+ my ($self, $mail) = @_;
+
+ my $dbhmails = $self->{dbstat}->{mails}->{dbh};
+ my $dbhusers = $self->{dbstat}->{users}->{dbh};
+
+ return undef if !$dbhmails || !$dbhusers;
+
+ $mail = lc($mail);
+
+ my $res = [];
+
+ my $cuid;
+ $dbhmails->get($mail, $cuid);
+ return undef if !$cuid;
+
+ my $rdata;
+ $dbhusers->get($cuid, $rdata);
+ return undef if !$rdata;
+
+ my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
+
+ push @$res, { primary => 1, email => $pmail };
+
+ my $key = 0 ;
+ my $value = "" ;
+ my $status = $dbhmails->seq($key, $value, R_FIRST());
+
+ while ($status == 0) {
+ if ($value == $cuid && $key ne $pmail) {
+ push @$res, { primary => 0, email => $key };
+ }
+ $status = $dbhmails->seq($key, $value, R_NEXT());
+ }
+
+ return $res;
}
sub mail_exists {
return $res;
}
+sub group_exists {
+ my ($self, $group) = @_;
+
+ my $dbh = $self->{dbstat}->{groups}->{dbh};
+ return 0 if !$dbh;
+
+ my $res;
+ $dbh->get($group, $res);
+ return $res;
+}
+
sub account_has_address {
my ($self, $account, $mail) = @_;