#!/usr/bin/perl
# 
# accesses-from-ldap-open.pl
# Written by Erik Inge Bolsø (knan@redpill-linpro.com), 2009.
#
# COPYRIGHT
# This script is free software, you can redistribute it and/or modify
# it under the same terms as Perl itself.
#
use strict;
use Net::LDAP;
use Net::LDAP::Constant qw(LDAP_COMPARE_FALSE LDAP_COMPARE_TRUE);

# debug code
#use IO::Handle;
#
#open ERROR, '>', "/tmp/error.txt" ;
#STDERR->fdopen( \*ERROR, 'w' ) ;

# overview:
#  grab username, ip from environment
#  construct dn, verify dn is in group or error out
#  grab vpnAccess attributes from dn
#  run ipset commands to add accesses

# config part
my $ldap_uri          = "ldap://localhost:389/";
my $ldap_base         = "dc=example,dc=org";
my $ldap_userfilter   = "(objectClass=posixAccount)";
my $ldap_group        = "(cn=EveryBodyRelevant)";
my $ldap_groupbase    = "ou=Groups," . $ldap_base ;
my $ldap_groupattr    = "uniqueMember" ;

# shouldn't need to customize below here
my $openvpn_username  = $ENV{'common_name'}               or die "a-f-l-o: script environment out of order, \$common_name missing.\n";
my $openvpn_remoteip  = $ENV{'ifconfig_pool_remote_ip'}   or die "a-f-l-o: script environment out of order, \$ifconfig_pool_remote_ip missing.\n";

my $ldap = Net::LDAP->new( $ldap_uri ) or die "LDAP connection failed: $!\n";

# find user dn (ldapsearch with username and filter)
my $mesg = $ldap->bind() ;

my $tmp_searchfilter = "(&" . $ldap_userfilter . "(uid=" . $openvpn_username . "))" ;
$mesg = $ldap->search( base   => $ldap_base,
                       filter => $tmp_searchfilter
                     );
# wrong ldap_base (among others) will hit this, shouldn't have hits after initial setup
$mesg->code && die "a-f-l-o: ldap search for " . $tmp_searchfilter . " @ base " . $ldap_base . " failed: " . $mesg->error . "\n";
# FIXME: what to do with uids found in multiple branches? We don't know which one openvpn-auth-ldap authenticated against.
if ( $mesg->count > 1 ) { die "a-f-l-o: ambiguous username " . $openvpn_username . ", " . $mesg->count . "hits, bailing out!\n" ; }
# if we get to this script, the username is already authenticated in ldap - if it's not found, there's a config error somewhere
if ( $mesg->count == 0 ) { die "a-f-l-o: username " . $openvpn_username . " not found in ldap, config error?\n" ; }

my $tmp_entry = $mesg->pop_entry() ;
my $ldap_user_dn = $tmp_entry->dn() ;

# verify dn in group (so we use same dn as openvpn-auth-ldap)

# find group
$mesg = $ldap->search( base   => $ldap_groupbase,
                       filter => $ldap_group
                     );
# wrong ldap_groupbase (among others) will hit this, shouldn't have hits after initial setup
$mesg->code && die "a-f-l-o: ldap search for " . $ldap_group . " @ base " . $ldap_groupbase . " failed: " . $mesg->error . "\n";
# ambiguous group configured? Panic!
if ( $mesg->count > 1 ) { die "a-f-l-o: ambiguous group " . $ldap_group . ", " . $mesg->count . "hits, bailing out!\n" ; }
# group not found? Panic!
if ( $mesg->count == 0 ) { die "a-f-l-o: group " . $ldap_group . " not found in ldap, config error...\n" ; }

$tmp_entry = $mesg->pop_entry() ;
my $ldap_group_dn = $tmp_entry->dn() ;

# check dn member of group
$mesg = $ldap->compare( $ldap_group_dn,
                        attr  => $ldap_groupattr,
                        value => $ldap_user_dn ) ;

if ($mesg->code == LDAP_COMPARE_FALSE) { die "a-f-l-o: user dn not member of group, bailing out...\n"; }
if ($mesg->code != LDAP_COMPARE_TRUE)  { die "a-f-l-o: error checking group membership: " . $mesg->error . "\n"; }


# grab user's vpnAccess attributes from LDAP
$mesg = $ldap->search( base   => $ldap_user_dn,
                       filter => "(objectClass=*)", # why does this not have a default? gnrf. Expect "Bad filter" errors if you omit this.
                       scope  => 'base',
                       attrs  => ['vpnAccess']
                     );

# did everything go wrong and explode?
$mesg->code && die "a-f-l-o: ldap search @ base " . $ldap_user_dn . " failed: " . $mesg->error . "\n";

my @accesses = ( ) ;

$tmp_entry = $mesg->pop_entry() ;
foreach my $attr ( $tmp_entry->attributes ) {
  push (@accesses, $tmp_entry->get_value( $attr )) ;
}

# may want to error out here if no accesses, depending on vpn policy

# add ip to the named ipset sets
my $panic = 0 ;
my $access_error;

foreach my $access ( @accesses ) {
  system("/usr/bin/sudo /usr/sbin/ipset -A $access $openvpn_remoteip") == 0 or do { $panic=1; $access_error = $access; } ;
}

# ipset command complained, remove accesses (ignore errors) and error out
if ($panic == 1)
{
  foreach my $access ( @accesses ) {
    system("/usr/bin/sudo /usr/sbin/ipset -D $access $openvpn_remoteip");
  }
  die "a-f-l-o: error adding access $access_error for user $openvpn_username\n" ;
}

# success!
exit 0;


