From eafdc1c6ac7683c547e4ee9eaecf0076af5141fb Mon Sep 17 00:00:00 2001 From: upstream Date: Sat, 3 Oct 2026 02:10:21 +0200 Subject: [PATCH] Upstream proxmox-git/pve-common state of the touched files before b40f26a995 --- src/PVE/LDAP.pm | 276 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 src/PVE/LDAP.pm diff --git a/src/PVE/LDAP.pm b/src/PVE/LDAP.pm new file mode 100644 index 0000000..dd8d22a --- /dev/null +++ b/src/PVE/LDAP.pm @@ -0,0 +1,276 @@ +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); +use Net::LDAP::Util qw(escape_filter_value); + +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, + ); + + 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 "$@\n"; + + if ($start_tls) { + my $res = $ldap->start_tls(%$opts); + die $res->error . "\n" if $res->code; + } + + 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) = @_; + + my $escaped_name = escape_filter_value($name); + # search for dn + my $result = $ldap->search( + base => $base_dn // "", + scope => "sub", + filter => "$attr=$escaped_name", + attrs => ['dn'], + ); + die $result->error . "\n" if $result->code; + return undef if !$result->entries; + my @entries = $result->entries; + return $entries[0]->dn; +} + +sub auth_user_dn { + my ($ldap, $dn, $pw, $noerr) = @_; + + if (!$dn) { + return undef if $noerr; + die "user dn is empty\n"; + } + + my $res = $ldap->bind($dn, password => $pw); + + my $code = $res->code; + my $err = $res->error; + + if ($code) { + return undef if $noerr; + die "$err\n"; + } + + return 1; +} + +sub query_users { + my ($ldap, $filter, $attributes, $base_dn, $classes) = @_; + + # build filter from given filter and attribute list + my $tmp = "(|"; + foreach my $att (@$attributes) { + $tmp .= "($att=*)"; + } + $tmp .= ")"; + + if ($classes) { + $tmp = "(&$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 => 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\n" if $err; + + return $users; +} + +sub query_groups { + my ($ldap, $base_dn, $classes, $filter, $group_name_attr) = @_; + + 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 $attrs = ['member', 'uniqueMember']; + push @$attrs, $group_name_attr if $group_name_attr; + my @args = ( + base => $base_dn, + scope => "subtree", + filter => $filter, + control => [$page], + attrs => $attrs, + ); + + 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; + if ($group_name_attr && (my $name = $entry->get_value($group_name_attr))) { + $group->{name} = $name; + } + 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\n" if $err; + + return $groups; +} + +1;