[pve-devel] [PATCH common 1/1] add LDAP Wrapper code
Dominik Csapak
d.csapak at proxmox.com
Thu Feb 20 16:20:00 CET 2020
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 at proxmox.com>
---
src/Makefile | 1 +
src/PVE/LDAP.pm | 254 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 255 insertions(+)
create mode 100644 src/PVE/LDAP.pm
diff --git a/src/Makefile b/src/Makefile
index 02f1f56..ada166d 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -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
index 0000000..cb88977
--- /dev/null
+++ b/src/PVE/LDAP.pm
@@ -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;
--
2.20.1
More information about the pve-devel
mailing list