diff options
author | Russ Allbery <eagle@eyrie.org> | 2018-06-03 16:58:04 -0700 |
---|---|---|
committer | Russ Allbery <eagle@eyrie.org> | 2018-06-03 16:58:04 -0700 |
commit | 52a00911071f6b1f61fc0a1f9c6f54bf38ab50a6 (patch) | |
tree | f60df4fa425feede71437ea91e6176369a5ae3b6 /perl/lib/Wallet/Kadmin/AD.pm | |
parent | 6054a5b5806ed1e529b9fbed1c2284580f2a01be (diff) | |
parent | edf31eba414d9a105791c076fb1444a78d210dff (diff) |
Update upstream source from tag 'upstream/1.4'
Update to upstream version '1.4'
with Debian dir 0b0d636e76769b309abb838da9361d95c611ebfe
Diffstat (limited to 'perl/lib/Wallet/Kadmin/AD.pm')
-rw-r--r-- | perl/lib/Wallet/Kadmin/AD.pm | 374 |
1 files changed, 228 insertions, 146 deletions
diff --git a/perl/lib/Wallet/Kadmin/AD.pm b/perl/lib/Wallet/Kadmin/AD.pm index 5b71d41..f2f86b9 100644 --- a/perl/lib/Wallet/Kadmin/AD.pm +++ b/perl/lib/Wallet/Kadmin/AD.pm @@ -1,12 +1,12 @@ # Wallet::Kadmin::AD -- Wallet Kerberos administration API for AD # -# Written by Bill MacAllister <bill@ca-zephyr.org> -# Copyright 2016 Russ Allbery <eagle@eyrie.org> -# Copyright 2015 Dropbox, Inc. -# Copyright 2007, 2008, 2009, 2010, 2014 +# Written by Bill MacAllister <whm@dropbox.com> +# Copyright 2016, 2018 Russ Allbery <eagle@eyrie.org> +# Copyright 2015-2016 Dropbox, Inc. +# Copyright 2007-2010, 2014 # The Board of Trustees of the Leland Stanford Junior University # -# See LICENSE for licensing terms. +# SPDX-License-Identifier: MIT ############################################################################## # Modules and declarations @@ -26,7 +26,9 @@ use Wallet::Config; use Wallet::Kadmin; our @ISA = qw(Wallet::Kadmin); -our $VERSION = '1.03'; +our $VERSION = '1.04'; + +my $LDAP; ############################################################################## # kadmin Interaction @@ -34,33 +36,47 @@ our $VERSION = '1.03'; # Send debugging output to syslog. -sub ad_debug { +sub ad_syslog { my ($self, $l, $m) = @_; if (!$self->{SYSLOG}) { openlog('wallet-server', 'ndelay,nofatal', 'local3'); $self->{SYSLOG} = 1; } + if ($l !~ /^(debug|info|err|warning)$/xms) { + $l = 'err'; + } syslog($l, $m); return; } +# Return a string given an array whose elements are command line arguments +# passws to IPC::Run. Quote any strings that have embedded spaces. Replace +# null elements with the string #NULL#. + +sub ad_cmd_string { + my ($self, $cmd_ref) = @_; + my $z = ''; + my $ws = ' '; + for my $e (@{ $cmd_ref }) { + if (!$e) { + $z .= $ws . '#NULL#'; + } elsif ($e =~ /\s/xms) { + $z .= $ws . '"' . $e . '"'; + } else { + $z .= $ws . $e; + } + $ws = ' '; + } + return $z; +} + # Make sure that principals are well-formed and don't contain # characters that will cause us problems when talking to kadmin. # Takes a principal and returns true if it's okay, false otherwise. # Note that we do not permit realm information here. sub valid_principal { my ($self, $principal) = @_; - my $valid = 0; - if ($principal =~ m,^(host|service)(/[\w_.-]+)?\z,) { - my $k_type = $1; - my $k_id = $2; - if ($k_type eq 'host') { - $valid = 1 if $k_id =~ m/[.]/xms; - } elsif ($k_type eq 'service') { - $valid = 1 if length($k_id) < 19; - } - } - return $valid; + return scalar ($principal =~ m,^[\w-]+(/[\w_.-]+)?\z,); } # Connect to the Active Directory server using LDAP. The connection is @@ -69,48 +85,111 @@ sub valid_principal { sub ldap_connect { my ($self) = @_; - if (!-e $Wallet::Config::AD_CACHE) { - die 'Missing kerberos ticket cache ' . $Wallet::Config::AD_CACHE; - } - - my $ldap; - eval { - local $ENV{KRB5CCNAME} = $Wallet::Config::AD_CACHE; - my $sasl = Authen::SASL->new(mechanism => 'GSSAPI'); - $ldap = Net::LDAP->new($Wallet::Config::KEYTAB_HOST, onerror => 'die'); - my $mesg = eval { $ldap->bind(undef, sasl => $sasl) }; - }; - if ($@) { - my $error = $@; - chomp $error; - 1 while ($error =~ s/ at \S+ line \d+\.?\z//); - die "LDAP bind to AD failed: $error\n"; + if (!$LDAP) { + eval { + local $ENV{KRB5CCNAME} = $Wallet::Config::AD_CACHE; + my $sasl = Authen::SASL->new(mechanism => 'GSSAPI'); + $LDAP = Net::LDAP->new($Wallet::Config::KEYTAB_HOST, + onerror => 'die'); + my $mesg = eval { $LDAP->bind(undef, sasl => $sasl) }; + }; + if ($@) { + my $error = $@; + chomp $error; + 1 while ($error =~ s/ at \S+ line \d+\.?\z//); + die "LDAP bind to AD failed: $error\n"; + } } - - return $ldap; + return $LDAP; } # Construct a base filter for searching Active Directory. sub ldap_base_filter { my ($self, $principal) = @_; + my $base; my $filter; - if ($principal =~ m,^host/(\S+),xms) { - my $fqdn = $1; - my $host = $fqdn; - $host =~ s/[.].*//xms; - $base = $Wallet::Config::AD_COMPUTER_DN; - $filter = "(samAccountName=${host}\$)"; - } elsif ($principal =~ m,^service/(\S+),xms) { - my $id = $1; - $base = $Wallet::Config::AD_USER_DN; - $filter = "(servicePrincipalName=service/${id})"; + my $this_type; + my $this_id; + + if ($principal =~ m,^(.*?)/(\S+),xms) { + $this_type = $1; + $this_id = $2; + } else { + $this_id = $principal; + } + + # Create a filter to find the objects we create + if ($this_id =~ s/@(.*)//xms) { + $filter = "(userPrincipalName=${principal})"; + } elsif ($Wallet::Config::KEYTAB_REALM) { + $filter = '(userPrincipalName=' . $principal + . '@' . $Wallet::Config::KEYTAB_REALM . ')'; + } else { + $filter = "(userPrincipalName=${principal}\@*)"; + } + + # Set the base distinguished name + if ($this_type && $this_type eq 'host') { + $base = $Wallet::Config::AD_COMPUTER_RDN; + } else { + $base = $Wallet::Config::AD_USER_RDN; } + $base .= ',' . $Wallet::Config::AD_BASE_DN; + return ($base, $filter); } -# TODO: Get a keytab from the keytab cache. +# Take in a base and a filter and return the assoicated DN or return +# null if there is no matching entry. +sub ldap_get_dn { + my ($self, $base, $filter) = @_; + my $dn; + + if ($Wallet::Config::AD_DEBUG) { + $self->ad_syslog('debug', "base:$base filter:$filter scope:subtree\n"); + } + + $self->ldap_connect(); + my @attrs = ('objectclass'); + my $result; + eval { + $result = $LDAP->search( + base => $base, + scope => 'subtree', + filter => $filter, + attrs => \@attrs + ); + }; + if ($@) { + my $error = $@; + die "LDAP search error: $error\n"; + } + if ($result->code) { + $self->ad_syslog('info', "base:$base filter:$filter scope:subtree\n"); + die $result->error; + } + if ($Wallet::Config::AD_DEBUG) { + $self->ad_syslog('debug', 'returned: ' . $result->count); + } + + if ($result->count == 1) { + for my $entry ($result->entries) { + $dn = $entry->dn; + } + } elsif ($result->count > 1) { + $self->ad_syslog('err', 'too many AD entries for this keytab'); + for my $entry ($result->entries) { + $self->ad_syslog('info', 'dn found: ' . $entry->dn . "\n"); + } + die("INFO: use show to examine the problem\n"); + } + + return $dn; +} + +# TODO: Get a keytab from the keytab bucket. sub get_ad_keytab { my ($self, $principal) = @_; return; @@ -125,13 +204,16 @@ sub get_ad_keytab { sub msktutil { my ($self, $args_ref) = @_; unless (defined($Wallet::Config::KEYTAB_HOST) + and defined($Wallet::Config::KEYTAB_PRINCIPAL) + and defined($Wallet::Config::KEYTAB_FILE) and defined($Wallet::Config::KEYTAB_REALM)) { die "keytab object implementation not configured\n"; } - unless (defined($Wallet::Config::AD_CACHE) - and defined($Wallet::Config::AD_COMPUTER_DN) - and defined($Wallet::Config::AD_USER_DN)) + unless (-e $Wallet::Config::AD_MSKTUTIL + and defined($Wallet::Config::AD_BASE_DN) + and defined($Wallet::Config::AD_COMPUTER_RDN) + and defined($Wallet::Config::AD_USER_RDN)) { die "Active Directory support not configured\n"; } @@ -139,7 +221,7 @@ sub msktutil { my @cmd = ($Wallet::Config::AD_MSKTUTIL); push @cmd, @args; if ($Wallet::Config::AD_DEBUG) { - $self->ad_debug('debug', join(' ', @cmd)); + $self->ad_syslog('debug', $self->ad_cmd_string(\@cmd)); } my $in; @@ -162,6 +244,7 @@ sub msktutil { $err_msg .= "ERROR: $err\n"; $err_msg .= 'Problem command: ' . join(' ', @cmd) . "\n"; } + $self->ad_syslog('err', $err_msg); die $err_msg; } else { if ($err) { @@ -169,49 +252,107 @@ sub msktutil { } } if ($Wallet::Config::AD_DEBUG) { - $self->ad_debug('debug', $out); + $self->ad_syslog('debug', $out); } return $out; } +# The unique identifier that Active Directory used to store keytabs +# has a maximum length of 20 characters. This routine takes a +# principal name an generates a unique ID based on the principal name. +sub get_account_id { + my ($self, $this_princ) = @_; + + my $this_id; + my ($this_base, $this_filter) = $self->ldap_base_filter($this_princ); + my $real_dn = $self->ldap_get_dn($this_base, $this_filter); + if ($real_dn) { + $this_id = $real_dn; + $this_id =~ s/,.*//xms; + $this_id =~ s/.*?=//xms; + } else { + my ($this_type, $this_cn) = split '/', $this_princ, 2; + my $max_len; + if ($this_type eq 'host') { + $max_len = $Wallet::Config::AD_SERVICE_LENGTH - 1; + } else { + $max_len = $Wallet::Config::AD_SERVICE_LENGTH; + if ($Wallet::Config::AD_SERVICE_PREFIX) { + $this_cn = $Wallet::Config::AD_SERVICE_PREFIX . $this_cn; + } + } + my $loop_limit = $Wallet::Config::AD_SERVICE_LIMIT; + if (length($this_cn)>$max_len) { + my $cnt = 0; + my $this_dn; + my $suffix_size = length("$loop_limit"); + my $this_prefix = substr($this_cn, 0, $max_len - $suffix_size); + my $this_format = "%0${suffix_size}i"; + while ($cnt<$loop_limit) { + $this_cn = $this_prefix . sprintf($this_format, $cnt); + $this_dn = $self->ldap_get_dn($this_base, "cn=$this_cn"); + if (!$this_dn) { + $this_id = $this_cn; + last; + } + $cnt++; + } + } else { + $this_id = $this_cn; + } + } + return $this_id; +} + # Either create or update a keytab for the principal. Return the # name of the keytab file created. sub ad_create_update { my ($self, $principal, $action) = @_; + return unless $self->valid_principal($principal); my $keytab = $Wallet::Config::KEYTAB_TMP . "/keytab.$$"; if (-e $keytab) { unlink $keytab or die "Problem deleting $keytab\n"; } my @cmd = ('--' . $action); push @cmd, '--server', $Wallet::Config::AD_SERVER; - push @cmd, '--enctypes', '0x4'; - push @cmd, '--enctypes', '0x8'; - push @cmd, '--enctypes', '0x10'; + push @cmd, '--enctypes', '0x1C'; push @cmd, '--keytab', $keytab; push @cmd, '--realm', $Wallet::Config::KEYTAB_REALM; - - if ($principal =~ m,^host/(\S+),xms) { - my $fqdn = $1; - my $host = $fqdn; - $host =~ s/[.].*//xms; - push @cmd, '--dont-expire-password'; - push @cmd, '--computer-name', $host; - push @cmd, '--upn', "host/$fqdn"; - push @cmd, '--hostname', $fqdn; - } elsif ($principal =~ m,^service/(\S+),xms) { - my $service_id = $1; - push @cmd, '--use-service-account'; - push @cmd, '--service', "service/$service_id"; - push @cmd, '--account-name', "srv-${service_id}"; - push @cmd, '--no-pac'; - } - my $out = $self->msktutil(\@cmd); - if ($out =~ /Error:\s+\S+\s+failed/xms) { - $self->ad_delete($principal); - my $m = "ERROR: problem creating keytab:\n" . $out; - $m .= 'INFO: the keytab used to by wallet probably has' - . " insufficient access to AD\n"; - die $m; + push @cmd, '--upn', $principal; + + my $this_type; + my $this_id; + if ($principal =~ m,^(.*?)/(\S+),xms) { + $this_type = $1; + $this_id = $2; + my $account_id = $self->get_account_id($principal); + if ($this_type eq 'host') { + my $host = $this_id; + $host =~ s/[.].*//xms; + push @cmd, '--base', $Wallet::Config::AD_COMPUTER_RDN; + push @cmd, '--dont-expire-password'; + push @cmd, '--computer-name', $account_id; + push @cmd, '--hostname', $this_id; + } else { + push @cmd, '--base', $Wallet::Config::AD_USER_RDN; + push @cmd, '--use-service-account'; + push @cmd, '--service', $principal; + push @cmd, '--account-name', $account_id; + push @cmd, '--no-pac'; + } + my $out = $self->msktutil(\@cmd); + if ($out =~ /Error:\s+\S+\s+failed/xms + || !$self->exists($principal)) + { + $self->ad_delete($principal); + my $m = "ERROR: problem creating keytab for $principal"; + $self->ad_syslog('err', $m); + $self->ad_syslog('err', + 'Problem command:' . ad_cmd_string(\@cmd)); + die "$m\n"; + } + } else { + die "ERROR: Invalid principal format ($principal)\n"; } return $keytab; @@ -234,45 +375,9 @@ sub exists { my ($self, $principal) = @_; return unless $self->valid_principal($principal); - my $ldap = $self->ldap_connect(); my ($base, $filter) = $self->ldap_base_filter($principal); - my @attrs = ('objectClass', 'msds-KeyVersionNumber'); - my $result; - eval { - $result = $ldap->search( - base => $base, - scope => 'subtree', - filter => $filter, - attrs => \@attrs - ); - }; - - if ($@) { - my $error = $@; - die "LDAP search error: $error\n"; - } - if ($result->code) { - my $m; - $m .= "INFO base:$base filter:$filter scope:subtree\n"; - $m .= 'ERROR:' . $result->error . "\n"; - die $m; - } - if ($result->count > 1) { - my $m = "ERROR: too many AD entries for this keytab\n"; - for my $entry ($result->entries) { - $m .= 'INFO: dn found ' . $entry->dn . "\n"; - } - die $m; - } - if ($result->count) { - for my $entry ($result->entries) { - return $entry->get_value('msds-KeyVersionNumber'); - } - } else { - return 0; - } - return; + return $self->ldap_get_dn($base, $filter); } # Call msktutil to Create a principal in Kerberos. Sets the error and @@ -287,7 +392,7 @@ sub create { } if ($self->exists($principal)) { if ($Wallet::Config::AD_DEBUG) { - $self->ad_debug('debug', "$principal exists"); + $self->ad_syslog('debug', "$principal exists"); } return 1; } @@ -345,7 +450,7 @@ sub destroy { } my $exists = $self->exists($principal); if (!defined $exists) { - return; + return 1; } elsif (not $exists) { return 1; } @@ -358,27 +463,16 @@ sub destroy { sub ad_delete { my ($self, $principal) = @_; - my $k_type; - my $k_id; - my $dn; - if ($principal =~ m,^(host|service)/(\S+),xms) { - $k_type = $1; - $k_id = $2; - if ($k_type eq 'host') { - my $host = $k_id; - $host =~ s/[.].*//; - $dn = "cn=${host}," . $Wallet::Config::AD_COMPUTER_DN; - } elsif ($k_type eq 'service') { - $dn = "cn=srv-${k_id}," . $Wallet::Config::AD_USER_DN; - } - } + my ($base, $filter) = $self->ldap_base_filter($principal); + my $dn = $self->ldap_get_dn($base, $filter); - my $ldap = $self->ldap_connect(); - my $msgid = $ldap->delete($dn); + $self->ldap_connect(); + my $msgid = $LDAP->delete($dn); if ($msgid->code) { my $m; $m .= "ERROR: Problem deleting $dn\n"; $m .= $msgid->error; + $self->ad_syslog('err', $m); die $m; } return 1; @@ -437,18 +531,6 @@ using a local keytab cache. To use this class, several configuration parameters must be set. See L<Wallet::Config/"KEYTAB OBJECT CONFIGURATION"> for details. -=head1 FILES - -=over 4 - -=item KEYTAB_TMP/keytab.<pid> - -The keytab is created in this file and then read into memory. KEYTAB_TMP -is set in the wallet configuration, and <pid> is the process ID of the -current process. The file is unlinked after being read. - -=back - =head1 LIMITATIONS Currently, this implementation calls an external B<msktutil> program rather @@ -461,7 +543,7 @@ msktutil, Wallet::Config(3), Wallet::Kadmin(3), Wallet::Object::Keytab(3), wallet-backend(8) This module is part of the wallet system. The current version is -available from L<http://www.eyrie.org/~eagle/software/wallet/>. +available from L<https://www.eyrie.org/~eagle/software/wallet/>. =head1 AUTHORS |