add LDAP Wrapper code
authorDominik Csapak <d.csapak@proxmox.com>
Thu, 20 Feb 2020 15:20:00 +0000 (16:20 +0100)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 4 Mar 2020 14:33:50 +0000 (15:33 +0100)
This will be used for PMG and PVE LDAP Authentication & Sync.
The code is largely copied/inspired by the already existing LDAP code in
PVEs AccessControl and PMGs LDAPCache

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
src/Makefile
src/PVE/LDAP.pm [new file with mode: 0644]

index 02f1f56..ada166d 100644 (file)
@@ -20,6 +20,7 @@ LIB_SOURCES = \
        Exception.pm \
        INotify.pm \
        JSONSchema.pm \
+       LDAP.pm \
        Network.pm \
        OTP.pm \
        PTY.pm \
diff --git a/src/PVE/LDAP.pm b/src/PVE/LDAP.pm
new file mode 100644 (file)
index 0000000..cb88977
--- /dev/null
@@ -0,0 +1,254 @@
+package PVE::LDAP;
+
+use strict;
+use warnings;
+
+use Net::IP;
+use Net::LDAP;
+use Net::LDAP::Control::Paged;
+use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED);
+
+sub ldap_connect {
+    my ($servers, $scheme, $port, $opts) = @_;
+
+    my $start_tls = 0;
+
+    if ($scheme eq 'ldap+starttls') {
+       $scheme = 'ldap';
+       $start_tls = 1;
+    }
+
+    my %ldap_opts = (
+       scheme => $scheme,
+       port => $port,
+       timeout => 10,
+       onerror => 'die',
+    );
+
+    my $hosts = [];
+    for my $host (@$servers) {
+       if (Net::IP::ip_is_ipv6($host)) {
+           push @$hosts, "[$host]";
+       } else {
+           push @$hosts, $host;
+       }
+    }
+
+    for my $opt (qw(clientcert clientkey capath cafile sslversion verify)) {
+       $ldap_opts{$opt} = $opts->{$opt} if $opts->{$opt};
+    }
+
+    my $ldap = Net::LDAP->new($hosts, %ldap_opts) || die $@;
+
+    if ($start_tls) {
+       $ldap->start_tls(%$opts);
+    }
+
+    return $ldap;
+}
+
+sub ldap_bind {
+    my ($ldap, $dn, $pw) = @_;
+
+    my $res;
+    if (defined($dn) && defined($pw)) {
+       $res = $ldap->bind($dn, password => $pw);
+    } else { # anonymous bind
+       $res = $ldap->bind();
+    }
+
+    my $code = $res->code;
+    my $err = $res->error;
+
+    die "ldap bind failed: $err\n" if $code;
+}
+
+sub get_user_dn {
+    my ($ldap, $name, $attr, $base_dn) = @_;
+
+    # search for dn
+    my $result = $ldap->search(
+       base    => $base_dn // "",
+       scope   => "sub",
+       filter  => "$attr=$name",
+       attrs   => ['dn']
+    );
+    return undef if !$result->entries;
+    my @entries = $result->entries;
+    return $entries[0]->dn;
+}
+
+sub auth_user_dn {
+    my ($ldap, $dn, $pw, $noerr) = @_;
+    my $res = $ldap->bind($dn, password => $pw);
+
+    my $code = $res->code;
+    my $err = $res->error;
+
+    if ($code) {
+       return undef if $noerr;
+       die $err;
+    }
+
+    return 1;
+}
+
+sub query_users {
+    my ($ldap, $filter, $attributes, $base_dn) = @_;
+
+    # build filter from given filter and attribute list
+    my $tmp = "(|";
+    foreach my $att (@$attributes) {
+       $tmp .= "($att=*)";
+    }
+    $tmp .= ")";
+
+    if ($filter) {
+       $filter = "($filter)" if $filter !~ m/^\(.*\)$/;
+       $filter = "(&${filter}${tmp})"
+    } else {
+       $filter = $tmp;
+    }
+
+    my $page = Net::LDAP::Control::Paged->new(size => 900);
+
+    my @args = (
+       base     => $base_dn // "",
+       scope    => "subtree",
+       filter   => $filter,
+       control  => [ $page ],
+       attrs    => [ @$attributes, 'memberOf'],
+    );
+
+    my $cookie;
+    my $err;
+    my $users = [];
+
+    while(1) {
+
+       my $mesg = $ldap->search(@args);
+
+       # stop on error
+       if ($mesg->code)  {
+           $err = "ldap user search error: " . $mesg->error;
+           last;
+       }
+
+       #foreach my $entry ($mesg->entries) { $entry->dump; }
+       foreach my $entry ($mesg->entries) {
+           my $user = {
+               dn => $entry->dn,
+               attributes => {},
+               groups => [$entry->get_value('memberOf')],
+           };
+
+           foreach my $attr (@$attributes) {
+               my $vals = [$entry->get_value($attr)];
+               if (scalar(@$vals)) {
+                   $user->{attributes}->{$attr} = $vals;
+               }
+           }
+
+           push @$users, $user;
+       }
+
+       # Get cookie from paged control
+       my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
+       $cookie = $resp->cookie;
+
+       last if (!defined($cookie) || !length($cookie));
+
+       # Set cookie in paged control
+       $page->cookie($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);
+       $ldap->search(@args);
+       $err = "LDAP user query unsuccessful" if !$err;
+    }
+
+    die $err if $err;
+
+    return $users;
+}
+
+sub query_groups {
+    my ($ldap, $base_dn, $classes, $filter) = @_;
+
+    my $tmp = "(|";
+    for my $class (@$classes) {
+       $tmp .= "(objectclass=$class)";
+    }
+    $tmp .= ")";
+
+    if ($filter) {
+       $filter = "($filter)" if $filter !~ m/^\(.*\)$/;
+       $filter = "(&${filter}${tmp})"
+    } else {
+       $filter = $tmp;
+    }
+
+    my $page = Net::LDAP::Control::Paged->new(size => 100);
+
+    my @args = (
+       base     => $base_dn,
+       scope    => "subtree",
+       filter   => $filter,
+       control  => [ $page ],
+       attrs    => [ 'member', 'uniqueMember' ],
+    );
+
+    my $cookie;
+    my $err;
+    my $groups = [];
+
+    while(1) {
+
+       my $mesg = $ldap->search(@args);
+
+       # stop on error
+       if ($mesg->code)  {
+           $err = "ldap group search error: " . $mesg->error;
+           last;
+       }
+
+       foreach my $entry ( $mesg->entries ) {
+           my $group = {
+               dn => $entry->dn,
+               members => []
+           };
+           my $members = [$entry->get_value('member')];
+           if (!scalar(@$members)) {
+               $members = [$entry->get_value('uniqueMember')];
+           }
+           $group->{members} = $members;
+           push @$groups, $group;
+       }
+
+       # Get cookie from paged control
+       my ($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last;
+       $cookie = $resp->cookie;
+
+       last if (!defined($cookie) || !length($cookie));
+
+       # Set cookie in paged control
+       $page->cookie($cookie);
+    }
+
+    if ($cookie) {
+       # We had an abnormal exit, so let the server know we do not want any more
+       $page->cookie($cookie);
+       $page->size(0);
+       $ldap->search(@args);
+       $err = "LDAP group query unsuccessful" if !$err;
+    }
+
+    die $err if $err;
+
+    return $groups;
+}
+
+1;