#! /usr/bin/perl
# This file is part of pam-modules.
# Copyright (C) 2014-2015, 2018 Sergey Poznyakoff
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use Net::LDAP;
use Sys::Hostname;

=head1 NAME

ldappubkey - get user public ssh keys from the LDAP database

=head1 SYNOPSIS

B<ldappubkey> I<LOGIN>

=head1 DESCRIPTION

Produces on the standard output public ssh keys for the user I<LOGIN>, each
on a separate line.  The program is designed for use with B<sshd>(8) version
6.2p1 or higher.  Public keys are obtained from a LDAP database.  The
configuration is looked up in the following files: B</etc/ldap.conf>,
B</etc/ldap/ldap.conf> and B</etc/openldap/ldap.conf>.  These files are
tried in this order and the first one of them that exists is read.
If the environment variable B<LDAP_CONF> is defined, the file it points
to is tried first.    

The following configuration statements are used (all keywords are
case-insensitive):

=over 4

=item B<uri> B<ldap[si]://>[I<name>[:I<port>]] ...>

Specifies the URI of the LDAP server (or servers) to connect to.  The default
is B<ldap://127.0.0.1>.    
    
=item B<base> I<DN>

Specifies the default base DN to use when performing ldap operations.
The base must be specified as a Distinguished Name in LDAP format.

=item B<binddn> I<DN>

Specifies the default bind DN to use.

=item B<bindpw> I<PASS>

Specifies the password to use with B<binddn>.    

=item B<uid> I<ATTR>

Name of the attribute to use instead of B<uid>. LDAP records are searched
using the filter defined with the B<PublicKeyFilter> attribute (see below).

=item B<ssl start_tls>

Use TLS

=item B<tls_cacert> I<FILE>

Specifies the file that contains certificates for all of the Certificate
Authorities the client will recognize. 

=item B<tls_cacertdir> I<DIR>

Path of a directory that contains Certificate Authority certificates in
separate individual files.  The B<tls_cacert> statement takes precedence
over B<tls_cacertdir>.

=item B<tls_cert> I<FILE>
    
Specifies the file that contains the client certificate.
    
=item B<tls_key> I<FILE>

Specifies the file that contains the private key that matches the
certificate stored in the B<tls_cert> file.    

=item B<tls_cipher_suite> I<SPEC>

Specifies acceptable cipher suite and preference order.    

=item B<tls_reqcert> I<LEVEL>

Specifies what checks to perform on server certificates in a TLS session.
I<LEVEL> is one of B<never>, B<allow>, B<try>, B<demand> or B<hard>.
    
=item B<PublicKeyAttribute> I<ATTR> [I<ATTR>...]

List of attributes that hold the public key. Default is C<grayPublicKey>.

=item B<PublicKeyFilter> I<FILTER>

LDAP filter used to retrieve the objects that may contain public keys. The
I<FILTER> string can contain the following variables

=over 8

=item B<$uid>

The value of B<uid> setting (see above).
    
=item B<$arg>

First command line argument.
    
=item B<$hostname>

Full hostname of the machine.    
    
=back

Default value is:

    (&(objectClass=posixAccount)($uid=$arg))
    
=back

=head1 OPTIONS

=over 4

=item B<-h>

Show program usage.

=item B<--help>

Show detailed help page.    
    
=back    
    
=head1 ENVIRONMENT

=over 4

=item B<LDAP_CONF>

If defined, names the alternative configuration file to read.

=item B<GITCONFIG_TEMPLATE>

Name of the template file to use instead of the default B<.gitconfig>.
    
=back    

=head1 SEE ALSO

B<sshd>(8), B<sshd_config>(5), B<ldap.conf>(5).

=head1 BUGS

LDAP filter string is hardcoded.    
    
=head1 AUTHOR

Sergey Poznyakoff <gray@gnu.org>    
    
=cut

# ###################################
# Configuration file handling
# ###################################

my %config = ('uri' => 'ldap://127.0.0.1', 'uid' => 'uid',
	      'publickeyattribute' => 'grayPublicKey',
	      'publickeyfilter' => '(&(objectClass=posixAccount)($uid=$arg))');

sub read_config_file($) {
    my $config_file = shift;
    my $file;
    my $line = 0;

    open($file, "<", $config_file) or die("cannot open $config_file: $!");
    while (<$file>) {
	++$line;
	chomp;
	s/^\s+//;
	s/\s+$//;
        s/#.*//;
	next if ($_ eq "");
	my @kwp = split(/\s+/, $_, 2);
	$config{lc($kwp[0])} = $kwp[1];
    }
    close($file);
}

sub get_fqdn_hostname {
    my $name = hostname();
    if (my ($fqdn) = gethostbyname($name)) {
	return $fqdn;
    } else {
	return $name;
    }
}

sub assert {
    my $mesg = shift;
    my $action = shift;
    die("An error occurred $action: ".$mesg->error) if ($mesg->code);
    return $mesg;
}

# ###################################
# MAIN
# ###################################

die "bad number of arguments; try perldoc $0 for more info"
    unless ($#ARGV == 0);

## Read configuration
my @config_files = ("/etc/ldap.conf", "/etc/ldap/ldap.conf",
		    "/etc/openldap/ldap.conf");
unshift @config_files, $ENV{LDAP_CONF} if defined($ENV{LDAP_CONF});

foreach my $file (@config_files) {
    if (-e $file) {
	read_config_file($file);
	last;
    }
}

my $ldap = Net::LDAP->new($config{'uri'})
    or die("Unable to connect to LDAP server $config{'uri'}: $!");

if ($config{ssl} eq 'start_tls') {
    my %args;
    
    $args{capath} = $config{tls_cacertdir}
        if (defined($config{tls_cacertdir}));
    $args{cafile} = $config{tls_cacert}
        if (defined($config{tls_cacert}));
    if ($config{tls_reqcert} eq 'none') {
	$args{verify} = 'never';
    } elsif ($config{tls_reqcert} eq 'allow') {
	$args{verify} = 'optional';
    } elsif ($config{tls_reqcert} eq 'demand'
	     or $config{tls_reqcert} eq 'hard') {
	$args{verify} = 'require';
    } elsif ($config{tls_reqcert} eq 'try') {
	$args{verify} = 'optional'; # FIXME: That's wrong
    }
    $args{clientcert} = $config{tls_cert}
        if (defined($config{tls_cert}));
    $args{clientkey} = $config{tls_key}
        if (defined($config{tls_key}));
    $args{ciphers} = $config{tls_cipher_suite}
        if (defined($config{tls_cipher_suite}));
    
    assert($ldap->start_tls, "TLS negotiation"); 
}

my @bindargs = ();
if (defined($config{'binddn'})) {
    push(@bindargs, $config{'binddn'});
    push(@bindargs, password => $config{'bindpw'})
	if defined($config{'bindpw'});
}
assert($ldap->bind(@bindargs), "binding to the server");

my @attrs = split /\s+/, $config{'publickeyattribute'};
my $filter = $config{'publickeyfilter'};
my $uid = "$config{'uid'}";
my $arg = $ARGV[0];
my $hostname = get_fqdn_hostname();
$filter =~ s/(?<!\\)(\$(?:uid|arg|hostname))/$1/eeg;

my $res = assert($ldap->search(base => $config{'base'},
			       filter => $filter,
			       attrs => \@attrs ),
		 "searching for $filter in $config{'base'}");

foreach my $entry ($res->entries()) {
    for (map { my $a = $entry->get_value($_, asref => 1); $a ? @$a : () } @attrs) {
	print "$_\n";
    }
}

# END


