package Net::OpenVPN::Manager::Plugin::LDAP;

use namespace::autoclean;
use Moose;
use Net::LDAP;
use MooseX::Types::Moose qw(Str Int ArrayRef);
use MooseX::Types::Structured qw(Dict);
use AnyEvent::Util qw(fork_call);
use Net::OpenVPN::Manager::Plugin;
use Template;

our $VERSION = '0.01';

with 'Net::OpenVPN::Manager::Authenticable';
with 'Net::OpenVPN::Manager::Connectable';

has 'servers' => (
    is => 'ro',
    isa => ArrayRef[Dict[address => Str, port => Int]],
    default => sub { [
            { address => 'localhost', port => 389 }
    ]; },
);

has 'binddn' => (
    is => 'ro',
    isa => 'Str',
    default => 'cn=admin,dc=mydomain,dc=org',
);

has 'bindpw' => (
    is => 'ro',
    isa => 'Str',
    default => 'mysecret',
);

has 'basedn' => (
    is => 'ro',
    isa => 'Str',
    default => 'ou=people,dc=mydomain,dc=org',
);

has 'filter' => (
    is => 'ro',
    isa => 'Str',
    default => '(&(objectClass=inetOrgPerson)(uid=[% username %]))',
);

has 'attr_mapping' => (
    is => 'ro',
    isa => 'HashRef[Str]',
    default => sub { {}; }
);

sub _gen_filter {
    my ($self, $client) = @_;

    my $tmpl = Template->new();
    my $filter;
    my $r = $tmpl->process(\$self->filter, { username => $client->username }, \$filter);
    return $r == 1 ? $filter : undef;
}

sub _connect_ldap {
    my ($self) = @_;
    my @servers = map { $_->{address}.":".($_->{port} || 389) } @{$self->servers};
    my $ldap = Net::LDAP->new(\@servers);
    my $mesg = $ldap->bind($self->binddn, password => $self->bindpw);

    return $mesg->code ? undef : $ldap;
}

sub authenticate {
    my ($self, $client) = @_;
    my $username = $client->username;
    my ($password) = $client->password;
    my $cv = AE::cv;

    my $filter = $self->_gen_filter($client);

    unless (defined $filter) {
        return PLUG_ERROR;
    }

    fork_call {
        my $ldap = $self->_connect_ldap;

        return "unable to connect to ldap servers" unless $ldap;

        my $res = $ldap->search(
            base => $self->basedn,
	        scope => 'sub',
	        filter => $filter,
	        attrs => []
        );

        if ($res->count != 1) {
            return "Unable to find user";
        }
        my $entry = $res->shift_entry;

        my $mesg = $ldap->bind($entry->dn, password => $password);
        if ($mesg->code) {
            return "Unable to bind() user";
        }

        return;
    } sub {
        my ($res) = @_;
        if ($res) {
            $self->log($res, $client);
            $cv->send(PLUG_ERROR);
        } else {
            $self->log("user authenticated", $client);
            $cv->send(PLUG_OK);
        }
    };

    $cv
}

sub connect {
    my ($self, $client) = @_;
    my $username = $client->username;
    my $cv = AE::cv;

    my $filter = $self->_gen_filter($client);

    unless (defined $filter) {
        return PLUG_ERROR;
    }

    fork_call {
        my $ldap = $self->_connect_ldap;

        return "unable to connect to ldap servers" unless $ldap;

        my $res = $ldap->search(
            base => $self->basedn,
	        scope => 'sub',
	        filter => $filter,
	        attrs => [keys(%{$self->attr_mapping})]
        );

        if ($res->count != 1) {
            return "Unable to find user";
        }
        my $entry = $res->shift_entry;

        # map attributes to user
        my $attrs = {};
        foreach my $attr (keys(%{$self->attr_mapping})) {
            my $k = $self->attr_mapping->{$attr};
            my $values = [];
            map { push(@$values, $_) } $entry->get_value($attr);
            $attrs->{$k} = $values;
        }

        return $attrs;
    } sub {
        my ($attrs) = @_;
        if ($attrs && ref $attrs eq 'HASH') {
            map { $client->set_attr($_, $attrs->{$_}) } keys(%$attrs);
            $self->log("attributes extracted", $client);
            $cv->send(PLUG_OK);
        } else {
            $self->log($attrs, $client) if ($attrs);
            $cv->send(PLUG_ERROR);
        }
    };

    $cv
}

__PACKAGE__->meta->make_immutable;

1;

__END__

=head1 NAME

Net::OpenVPN::Manager::Plugin::LDAP - LDAP authentication plugin for openvpn-manager

=head1 SYNOPSIS

The LDAP plugin implements C<Authenticable> and C<Connectable> roles.

  plugins_config:
    LDAP:
      servers:
        - address: localhost
          port: 389
      binddn: cn=admin,dc=mydomain,dc=org
      bindpw: mysecret
      basedn: ou=people,dc=mydomain,dc=org
      filter: (&(objectClass=inetOrgPerson)(uid=[% username %]))
      attr_mapping:
        objectClass: ldap_classes

=head1 Attributes

=head2 servers

A array of LDAP servers. Each server must be a hash with C<address> and C<port> keys.

=head2 binddn

The DN of a user to bind to the ldap servers.

=head2 bindpw

The password of a user to bind to the ldap servers.

=head2 basedn

The base DN to search in the directory.

=head2 filter

The LDAP filter used to search a user. It must be a C<Template> string. A data
hash is provided to the template containing C<username> key.

=head2 attr_mapping

Map LDAP attributes into the client object to be used by other plugins. Must be
a hash where key is a LDAP attribute and value is a client attribute name.

=cut

