#! /usr/bin/perl # Copyright (C) 2014 by P. Tomulik # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Dependencies: # perl-base # libnet-ldap-perl # libdata-dump-perl use Net::LDAP; use Data::Dump; use Getopt::Long; use String::Escape qw( unbackslash ); my $attrs; my $base; my $binddn; my $binddn_env; my $bindpw; my $bindpw_env; my $debug = 0; my $deref; my $extra_filter; my $filter; my $group; my $ldapuri; my $multiple; my $starttls; my $passwd; my $passwd_env; my $print; my $rebind; my $scope; my $user; my $user_attr = 'uid'; my $user_env; my $user_filter; my $verbose; my $separator = '\n'; my $value_map; my $tag = 'openxpki-auth-ldap'; Getopt::Long::Configure('no_auto_abbrev', 'no_ignore_case', 'auto_help'); Getopt::Long::GetOptions( 'attrs|a=s' => \$attrs, 'base|b=s' => \$base, 'binddn|d=s' => \$binddn, 'binddn-env|D=s' => \$binddn_env, 'bindpw|w=s' => \$bindpw, 'bindpw-env|W=s' => \$bindpw_env, 'debug|g' => \$debug, 'deref=s' => \$deref, 'extra-filter=s' => \$extra_filter, 'filter|f=s' => \$filter, 'group|G=s' => \$group, 'ldapuri|H=s' => \$ldapuri, 'multiple|m' => \$multiple, 'starttls|t' => \$starttls, 'passwd|p=s' => \$passwd, 'passwd-env|P=s' => \$passwd_env, 'print=s' => \$print, 'rebind|r' => \$rebind, 'scope|s=s' => \$scope, 'user|u=s' => \$user, 'user-attr=s' => \$user_attr, 'user-env|U=s' => \$user_env, 'user-filter=s' => \$user_filter, 'verbose|V' => \$verbose, 'separator=s' => \$separator, 'value-map|k=s%' => \$value_map ) or exit(1); if($debug) { print STDERR "$tag: debug: initial values: \n"; print STDERR "$tag: debug: attrs: " . Data::Dump::dump($attrs) ."\n" if(defined($attrs)); print STDERR "$tag: debug: base: '$base'\n" if(defined($base)); print STDERR "$tag: debug: binddn: '$binddn'\n" if(defined($binddn)); print STDERR "$tag: debug: binddn_env: \$$binddn_env\n" if(defined($binddn_env)); print STDERR "$tag: debug: bindpw: '$bindpw'\n" if(defined($bindpw)); print STDERR "$tag: debug: bindpw_env: \$$bindpw_env\n" if(defined($bindpw_env)); print STDERR "$tag: debug: debug: $debug\n"; print STDERR "$tag: debug: deref: '$deref'\n" if(defined($deref)); print STDERR "$tag: debug: extra_filter: '$extra_filter'\n" if(defined($extra_filter)); print STDERR "$tag: debug: filter: '$filter'\n" if(defined($filter)); print STDERR "$tag: debug: group: '$group'\n" if(defined($group)); print STDERR "$tag: debug: ldapuri: '$ldapuri'\n" if(defined($ldapuri)); print STDERR "$tag: debug: multiple: '$multiple'\n" if(defined($multiple)); print STDERR "$tag: debug: starttls: '$starttls'\n" if(defined($starttls)); print STDERR "$tag: debug: passwd: '$passwd'\n" if(defined($passwd)); print STDERR "$tag: debug: passwd_env: \$$passwd_env\n" if(defined($passwd_env)); print STDERR "$tag: debug: print: '$print'\n" if(defined($print)); print STDERR "$tag: debug: rebind: $rebind\n" if(defined($rebind)); print STDERR "$tag: debug: scope: '$scope'\n" if(defined($scope)); print STDERR "$tag: debug: user: '$user'\n" if(defined($user)); print STDERR "$tag: debug: user_attr: '$user_attr'\n" if(defined($user)); print STDERR "$tag: debug: user_env: \$$user_env\n" if(defined($user_env)); print STDERR "$tag: debug: user_filter: '$user_filter'\n" if(defined($user_filter)); print STDERR "$tag: debug: verbose: $verbose\n" if(defined($verbose)); print STDERR "$tag: debug: separator: $separator\n" if(defined($separator)); print STDERR "$tag: debug: value_map: " . Data::Dump::dump($value_map) . "\n" if(defined($value_map)); } if(!defined($ldapuri)) { print STDERR "$tag: error: missing LDAP URI, you MUST specify it with -H or --ldapuri\n"; exit(1); } if(defined($binddn) && defined($binddn_env)) { print STDERR "$tag: error: --binddn and --binddn-env can't be specified at the same time\n"; exit(1); } if(defined($bindpw) && defined($bindpw_env)) { print STDERR "$tag: error: --bindpw and --bindpw-env can't be specified at the same time\n"; exit(1); } if(defined($binddn_env) && !exists($ENV{$binddn_env})) { print STDERR "$tag: error: expected environment variable \$$binddn_env to contain bind DN but it's not set\n"; exit(1); } if(defined($bindpw_env) && !exists($ENV{$bindpw_env})) { print STDERR "$tag: error: expected environment variable \$$bindpw_env to contain bind DN password but it's not set\n"; exit(1); } if(defined($user) && defined($user_env)) { print STDERR "$tag: error: --user and --user-env can't be specified at the same time\n"; exit(1); } if(defined($user) && defined($user_filter)) { print STDERR "$tag: error: --user and --user-filter can't be used at the same time\n"; exit(1); } if(defined($user) && defined($filter)) { print STDERR "$tag: error: --user and --filter can't be used at the same time\n"; exit(1); } if(defined($user_filter) && defined($filter)) { print STDERR "$tag: error: --user-filter and --filter can't be used at the same time\n"; exit(1); } if(defined($extra_filter) && defined($filter)) { print STDERR "$tag: error: --extra-filter and --filter can't be used at the same time\n"; exit(1); } if(defined($passwd) && defined($passwd_env)) { print STDERR "$tag: error: --passwd and --passwd-env can't be specified at the same time\n"; exit(1); } if(defined($user_env) && !exists($ENV{$user_env})) { print STDERR "$tag: error: expected environment variable \$$user_env to contain user user but it's not set\n"; exit(1); } if(defined($passwd_env) && !exists($ENV{$passwd_env})) { print STDERR "$tag: error: expected environment variable \$$passwd_env to contain user password but it's not set\n"; exit(1); } # Retrieve credentials from environment # $binddn = $ENV{$binddn_env} if(defined($binddn_env)); $bindpw = $ENV{$bindpw_env} if(defined($bindpw_env)); $user = $ENV{$user_env} if(defined($user_env)); $passwd = $ENV{$passwd_env} if(defined($passwd_env)); if(defined($user) && !defined($user_attr)) { print STDERR "--user or --user-env given but --user-attr is empty\n"; exit(1); } if(defined($user_attr) && defined($user)) { print STDERR "$tag: info: authenticating user '$user'\n" if($verbose); $user =~ s/\\/\\5C/g; $user =~ s/\*/\\2A/g; $user_filter = "$user_attr=$user"; } if(defined($user_filter)) { if(defined($extra_filter)) { $filter = "(&($user_filter)($extra_filter))"; } else { $filter = "($user_filter)"; } print STDERR "$tag: info: search filter set to '$filter'\n" if($verbose); } if(defined($attrs)) { my @tmp = split(/,/,$attrs); $attrs = undef; @$attrs = @tmp; } my $groupattr; my $groupdn; if(defined($group)) { my ($p1, $p2) = split(/\//,$group,2); if(!$p2) { $groupdn = $p1; } else { $groupattr = $p1; $groupdn = $p2; } $groupattr = 'member' if(!$groupattr); if($debug) { print STDERR "$tag: debug: \$groupdn='$groupdn'\n"; print STDERR "$tag: debug: \$groupattr='$groupattr'\n"; } if(!$groupdn) { print STDERR "$tag: error: group DN not provided for -G option\n"; exit(1); } } if($debug) { print STDERR "$tag: debug: final values: \n"; print STDERR "$tag: debug: attrs: " . Data::Dump::dump($attrs) ."\n" if(defined($attrs)); print STDERR "$tag: debug: base: '$base'\n" if(defined($base)); print STDERR "$tag: debug: binddn: '$binddn'\n" if(defined($binddn)); print STDERR "$tag: debug: binddn_env: \$$binddn_env\n" if(defined($binddn_env)); print STDERR "$tag: debug: bindpw: '$bindpw'\n" if(defined($bindpw)); print STDERR "$tag: debug: bindpw_env: \$$bindpw_env\n" if(defined($bindpw_env)); print STDERR "$tag: debug: debug: $debug\n"; print STDERR "$tag: debug: deref: '$deref'\n" if(defined($deref)); print STDERR "$tag: debug: extra_filter: '$extra_filter'\n" if(defined($extra_filter)); print STDERR "$tag: debug: filter: '$filter'\n" if(defined($filter)); print STDERR "$tag: debug: group: '$group'\n" if(defined($group)); print STDERR "$tag: debug: groupdn: '$groupdn'\n" if(defined($groupdn)); print STDERR "$tag: debug: groupattr: '$groupattr'\n" if(defined($groupattr)); print STDERR "$tag: debug: ldapuri: '$ldapuri'\n" if(defined($ldapuri)); print STDERR "$tag: debug: multiple: '$multiple'\n" if(defined($multiple)); print STDERR "$tag: debug: starttls:: '$starttls'\n" if(defined($starttls)); print STDERR "$tag: debug: passwd: '$passwd'\n" if(defined($passwd)); print STDERR "$tag: debug: passwd_env: \$$passwd_env\n" if(defined($passwd_env)); print STDERR "$tag: debug: print: '$print'\n" if(defined($print)); print STDERR "$tag: debug: rebind: $rebind\n" if(defined($rebind)); print STDERR "$tag: debug: scope: '$scope'\n" if(defined($scope)); print STDERR "$tag: debug: user: '$user'\n" if(defined($user)); print STDERR "$tag: debug: user_attr: '$user_attr'\n" if(defined($user)); print STDERR "$tag: debug: user_env: \$$user_env\n" if(defined($user_env)); print STDERR "$tag: debug: user_filter: '$user_filter'\n" if(defined($user_filter)); print STDERR "$tag: debug: verbose: $verbose\n" if(defined($verbose)); print STDERR "$tag: debug: separator: $separator\n" if(defined($separator)); print STDERR "$tag: debug: value_map: " . Data::Dump::dump($value_map) . "\n" if(defined($value_map)); } if($debug) { print STDERR "$tag: debug: connecting to $ldapuri\n"; print STDERR "$tag: debug: \$ldap = Net::LDAP->new('$ldapuri', verify => require)\n"; } my $ldap = Net::LDAP->new($ldapuri, verify => 'require') or die "$tag: error: $@"; if($starttls) { if($debug) { print STDERR "$tag: debug: Asking for Start TLS\n"; print STDERR "$tag: debug: \$ldap->start_tls( verify => 'require' )\n"; } $ldap->start_tls( verify => 'require' ) or die "$tag: error: $@"; } if($binddn && $bindpw) { print STDERR "$tag: debug: \$bind = \$ldap->bind('$binddn', password => '$bindpw')\n" if($debug);; $bind = $ldap->bind($binddn, password => $bindpw); } else { $bind = $ldap->bind(); } if($debug) { print STDERR "$tag: debug: \$bind->is_error() == ".$bind->is_error()."\n"; print STDERR "$tag: debug: \$bind->code == ".$bind->code."\n"; } if($bind->is_error()) { print STDERR "$tag: error: ".$bind->error()."\n" if($verbose); exit($bind->code); } my $userdn; if(defined($filter)) { print STDERR "$tag: debug: search filter given - will search LDAP database to find user\n" if($debug); my $args = {}; $args->{'attrs'} = []; # we're just searching for user DN $args->{'base'} = $base if(defined($base)); $args->{'scope'} = $base if(defined($scope)); $args->{'deref'} = $base if(defined($deref)); $args->{'filter'} = $filter; print STDERR "$tag: debug: \$result = \$ldap->search(". Data::Dump::dump($args) .")\n" if($debug); $result = $ldap->search(%$args); if($result->is_error()) { print STDERR "$tag: error: ".$result->error()."\n" if($verbose); exit($result->code); } print STDERR "$tag: debug: \$result->count == ". $result->count ."\n" if($debug);; if($result->count == 0) { print STDERR "$tag: error: user not found in database\n" if($verbose); exit(1); } elsif(!$multiple && $result->count > 1) { print STDERR "$tag: error: ambiguous search result (found ".$result->count."entries)\n" if($verbose); exit(1); } my @entries = $result->entries(); # Try all entries to bind to their DNs my $i = 0; foreach(@entries) { my $entry = $_; my $dn = $entry->dn(); if($debug) { print STDERR "$tag: debug: [$i] trying " .$dn. "\n" if($debug); print STDERR "$tag: debug: [$i] \$bind = \$ldap->bind('$dn', password => '$passwd')\n"; } # Try to bind as user my $bind = $ldap->bind($dn, password => $passwd); print STDERR "$tag: debug: [$i] \$bind->is_error() == ". $bind->is_error() ."\n" if($debug); if($bind->is_error()) { print STDERR "$tag: debug: [$i] ".$bind->error()."\n" if($debug); } else { print STDERR "$tag: debug: [$i] bind successful\n" if($debug); $userdn = $dn; } # Bind back to the original bind DN if($binddn && $bindpw) { if($debug) { print STDERR "$tag: debug: [$i] binding back to $binddn\n"; print STDERR "$tag: debug: [$i] \$bind = \$ldap->bind('$binddn', password => '$bindpw')\n"; } $bind = $ldap->bind($binddn, password => $bindpw); } else { $bind = $ldap->bind; } if($debug) { print STDERR "$tag: debug: [$i] \$bind->is_error() == ".$bind->is_error()."\n"; print STDERR "$tag: debug: [$i] \$bind->code == ".$bind->code."\n"; } if($bind->is_error()) { print STDERR "$tag: error: ".$bind->error()."\n" if($verbose); exit($bind->code); } # User initially authenticated, it's done unless we have to check its # group participation. if(defined($userdn)) { if(defined($groupdn) && defined($groupattr)) { # Group check if($debug) { print STDERR "$tag: debug: [$i] group check requested by user\n"; print STDERR "$tag: debug: [$i] \$result = \$ldap->compare('$groupdn', attr => '$groupattr', value => '$userdn')\n"; } my $result = $ldap->compare($groupdn, attr => $groupattr, value => $userdn); print STDERR "$tag: debug: [$i] \$result->is_error() == " . $result->is_error() . "\n" if($debug); if($result->is_error()) { print STDERR "$tag: error: ".$bind->error()."\n" if($verbose); exit($result->code); } print STDERR "$tag: debug: [$i] \$result->code == " . $result->code . "\n" if($debug); if($result->code == 6) { # compareTrue(6) print STDERR "$tag: debug: [$i] user belongs to the group $group\n" if($debug); last; } else { print STDERR "$tag: debug: [$i] user does not belong to the group $group\n" if($debug); $userdn = undef; } } else { last; } } $i += 1; } if(!defined($userdn)) { print STDERR "$tag: error: authentication failed\n" if($verbose); exit(1); } } elsif($binddn && $bindpw) { $userdn = $binddn; } else { print STDERR "$tag: debug: no binddn, username nor search filter specified\n" if($debug); print STDERR "$tag: error: authentication failed\n" if($verbose); exit(1); } # # Success! Do postprocessing. # print STDERR "$tag: info: successfully authenticated as '$userdn'\n" if($verbose); if(defined($print)) { print STDERR "$tag: debug: print was requested by user\n" if($debug); if($print =~ /%\{[a-zA-Z0-9_]+\}/) { print STDERR "$tag: debug: print template contains placeholders -- will retrieve user attributes\n" if($debug); if($rebind) { if($debug) { print STDERR "$tag: debug: rebind requested by user\n"; print STDERR "$tag: debug: \$bind = \$ldap->bind('$userdn', password => '$passwd')\n"; } my $bind = $ldap->bind($userdn, password => $passwd); print STDERR "$tag: debug: \$bind->is_error() == ". $bind->is_error() ."\n" if($debug); if($bind->is_error()) { print STDERR "$tag: debug: ".$bind->error()."\n" if($debug); } else { print STDERR "$tag: debug: bind successful\n" if($debug); } } my $args = { 'base' => $userdn, 'scope' => 'base', 'filter' => "objectClass=*" }; $args->{'attrs'} = $attrs if(defined($attrs)); print STDERR "$tag: debug: \$result = \$ldap->search(" .Data::Dump::dump($args). ");\n" if($debug); my $result = $ldap->search(%$args); print STDERR "$tag: debug: \$result->is_error() == " . $result->is_error() . "\n" if($debug); if($result->is_error()) { print STDERR "$tag: error: ".$result->error()."\n" if($verbose); exit($result->code); } print STDERR "$tag: debug: \$result->count() == " . $result->count() . "\n" if($debug); if($result->count > 1) { # Here we expect unique result, no matter what .. print STDERR "$tag: error: ambiguous search result for dn: $userdn\n" if($verbose); exit(1); } elsif($result->count == 0) { print STDERR "$tag: error: dn: $userdn not found in database\n" if($verbose); exit(1); } my @entries = $result->entries(); my $userentry = @entries[0]; print STDERR "$tag: debug: substituting s/%{dn}/$userdn/gi\n" if($debug); $print =~ s/%\{dn\}/$userdn/gi; foreach my $attr ($userentry->attributes) { my @values = $userentry->get_value($attr); if($print =~ /%\{$attr\}/) { if($debug) { print STDERR "$tag: debug: substituting s/%{$attr}/$_/gi\n" foreach (@values); } # Map raw attr values with the attribute mapping print STDERR "$tag: debug: Applying value mapping\n" if($debug); @values = map { defined $value_map->{$_} ? $value_map->{$_} : $_ } @values; $print =~ s/%{$attr}/join(unbackslash($separator),@values)/egi } } } print STDERR "$tag: debug: printing the requested string to stdout\n" if($debug); print "$print\n"; } exit(0); __END__ =head1 NAME openxpki-auth-ldap - Authenticate user against LDAP server. =head1 SYNOPSIS openxpki-auth-ldap B<-H> URI [options] Options: --attrs,-a attrs user attributes to retrieve from LDAP --base,-b searchbase base DN for user search --binddn,-d binddn bind DN used to bind to LDAP directory --binddn-env,-D name name of environment variable providing the binddn --bindpw,-w passwd use this password for bind DN authentication --bindpw-env,-W name name of environment variable providint bindpw --debug,-g run in debug mode --deref type specify how aliases dereferencing is done --extra-filter filter extra filter used when searching LDAP --filter filter hard-coded filter used to find the user --group,-G group ensure that the user belongs to a group --help print this help and exit --ldapuri,-H ldapuri URI referring to LDAP server --multiple,-m accept ambiguous search results --passwd,-p passwd password to be checked --passwd-env,-P name name of environment variable providing passwd --print template print a string specified by template --scope,-s scope ldap search scope: base, one, sub, children --user,-u username user name of the user to be authenticated --user-attr attr name of LDAP username attribute (default: uid) --user-env,-U name name of environment variable providing username --user-filter,-u filter hard-coded ldap filter to find user in database --verbose,-V print errors/warnings to stderr --separator used with --print, set the separator for multi-valued attributes --value-map,-k map attribute values with another value =head1 OPTIONS =over 8 =item B<--attrs,-a> I[,I[,...]] List of user attributes to retrieve from LDAP when B<--print> is requested. The attributes are only retrieved when B<--print> option is used and the print template contains placeholders (see B<--print>). By default all attributes are retrieved from LDAP. The B<--attrs> option may be used to limit the list of attributes being queried. The value is a comma-separated list of attribute names. Example: openxpki-auth-ldap --attrs cn,gidNumber,telephoneNumber ... =item B<--base,-b> I Use I as starting point for user search. This is only used when B<--user>, B<--user-filter>, or B<--filter> is specified (otherwise the user DN is specified directly by B<--binddn> and the search step is not performed). =item B<--binddn,-d> I Use Distinguished Name I to bind to the LDAP directory. This option is provided for troubleshooting purposes only. You should avoid passing bind credentials via command line. In production use B<--binddn-env> instead. =item B<--binddn-env,-D> I Use environment variable I to pass bind DN to the script. Example: (export BINDDN='cn=admin,dc=example,dc=com'; openxpki-auth-ldap -D BINDDN ...) =item B<--bindpw,-w> I Use I as the password for simple authentication (for binding as binddn). This option is provided for troubleshooting purposes only. You should NEVER PASS PASSWORDS VIA COMMAND LINE. In production use B<--bindpw-env> instead. =item B<--bindpw-env,-W> I Use environment variable I to provide a password to be used for binding with binddn. Example: (export BINDPW='secret'; openxpki-auth-ldap -W BINDPW ...) =item B<--debug,-g> Print debugging information to stderr. =item B<--deref> I Specify how aliases dereferencing is done. The I should be one of never, always, search, or find to specify that aliases are never dereferenced, always dereferenced, dereferenced when searching, or dereferenced only when locating the base object for the search. The default is to never dereference aliases. =item B<--extra-filter> I For use with --user or --user-filter. When constructing a search filter for user search it may be composed of two elements. The first is user_filter, which by assumption only places restrictions on user identifiers (e.g. uid). The second part called extra_filter may be arbitrary and provides additional, custom restrictions. The two parts: the B<--user-filter> and B<--extra-filter> are combined by the B operator. This flag conflicts with B<--filter>. Example: The flags: --user-filter 'uid=jsmith' --extra-filter 'accountStatus=active'" will result with the search filter: '(&(uid=jsmith)(accountStatus=active))' =item B<--filter,-f> I Hard-coded filter for user search. This flag conflicts with --user, --user-filter, and --extra-filter. =item B<--group,-G> I Ensure that user belongs to a given group. The I parameter specifies an entry containing Distinguished Names of users belonging to the group. The format of I argument is: [attr/]dn where C is the name of attribute collecting DNs of the group participants and C is a Distinguished Name of the group entry. If C is not provided, the default attribute C<'member'> is assumed. Example: # Let's say, we have the following group definition in LDAP: # # dn: cn=vip,dc=example,dc=org # cn: pm-users # objecClass: top # objecClass: organizationalRole # roleOccupant: uid=jsmith,ou=people,dc=example,dc=org # roleOccupant: uid=pbrown,ou=people,dc=example,dc=org # # Then we may restrict authentication to this group participants only # by using the following syntax: openxpki-auth-ldap -G "roleOccupant/cn=vip,dc=example,dc=org" ... =item B<--help> Print this help message. =item B<--ldapuri,-H> I Specify URI referring to the ldap server; only the protocol/host/port fields are allowed. =item B<--multiple,-m> Accept ambiguous search results. If the user search results with multiple entries, the script will try to bind to each of them (in order) untill first successful bind. By default ambiguous search results are discarded, that is they're reported as error. =item B<--starttls,-t> Upgrade the connection using Start TLS. If set, Start TLS is ran just after the connection is established, before any bind or search operation. =item B<--passwd,-p> I Provide password for user (specified with B<--user>, B<--user-env>, B<--user-filter> or B<--filter>). This option is provided for troubleshooting purposes only. You should NEVER PASS PASSWORDS VIA COMMANDLINE in production. Instead, you should use B<--passwd-env>. =item B<--passwd-env,-P> I Specify name of an environment variable carrying password for the user being authenticated (specified with either --user or --user-env). Example: (export PASSWD=secret; openxpki-auth-ldap --passwd-env PASSWD ...) =item B<--print> I