lemonldap-ng/lemonldap-ng-common/lib/Lemonldap/NG/Common/Notifications/LDAP.pm

550 lines
15 KiB
Perl

## @file
# LDAP storage methods for notifications
## @class
# LDAP storage methods for notifications
package Lemonldap::NG::Common::Notifications::LDAP;
use strict;
use Mouse;
use Time::Local;
use MIME::Base64 qw/encode_base64url/;
use Net::LDAP;
use utf8;
our $VERSION = '2.1.0';
extends 'Lemonldap::NG::Common::Notifications';
sub import {
shift;
return Lemonldap::NG::Common::Notifications->import(@_);
}
has ldapServer => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{ldapServer};
}
);
has ldapPort => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{ldapPort};
}
);
has ldapCAFile => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{ldapCAFile};
}
);
has ldapCAPath => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{ldapCAPath};
}
);
has ldapVerify => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{ldapVerify};
}
);
has ldapConfBase => (
is => 'ro',
trigger => sub {
if ( my $table = $_[0]->{table} ) {
$_[0]->{ldapConfBase} =~ s/^\w+=\w+(,.*)$/ou=$table$1/;
}
}
);
has ldapBindDN => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{managerDn};
}
);
has ldapBindPassword => (
is => 'ro',
lazy => 1,
default => sub {
$_[0]->conf->{managerPassword};
}
);
# Returns notifications corresponding to the user $uid.
# If $ref is set, returns only notification corresponding to this reference.
sub get {
my ( $self, $uid, $ref ) = @_;
return () unless ($uid);
my $filter =
'(&(objectClass=applicationProcess)(!(description={done}*))'
. "(description={uid}$uid)"
. ( $ref ? '(description={ref}' . $ref . ')' : '' ) . ')';
my @entries = _search( $self, $filter );
my $result;
foreach my $entry (@entries) {
my @notifValues = $entry->get_value('description');
my $f = {};
foreach (@notifValues) {
my ( $k, $v ) = ( $_ =~ /\{(.*?)\}(.*)/smg );
$v = decodeLdapValue($v);
$f->{$k} = $v;
}
my $xml = $f->{xml};
utf8::encode($xml);
my $identifier =
&getIdentifier( $self, $f->{uid}, $f->{ref}, $f->{date} );
$result->{$identifier} = "$xml";
$self->logger->info("notification $identifier found");
}
return $result;
}
# Returns accepted notifications corresponding to the user $uid.
# If $ref is set, returns only notification corresponding to this reference.
sub getAccepted {
my ( $self, $uid, $ref ) = @_;
return () unless ($uid);
my $filter =
'(&(objectClass=applicationProcess)(description={done}*)'
. "(description={uid}$uid)(description={ref}$ref))";
my @entries = _search( $self, $filter );
my $result;
foreach my $entry (@entries) {
my @notifValues = $entry->get_value('description');
my $f = {};
foreach (@notifValues) {
my ( $k, $v ) = ( $_ =~ /\{(.*?)\}(.*)/smg );
$v = decodeLdapValue($v);
$f->{$k} = $v;
}
my $xml = $f->{xml};
utf8::encode($xml);
my $identifier =
&getIdentifier( $self, $f->{uid}, $f->{ref}, $f->{date} );
$result->{$identifier} = "$xml";
$self->logger->info("notification $identifier found");
}
return $result;
}
## @method hashref getAll()
# Return all pending notifications.
# @return hashref where keys are internal reference and values are hashref with
# keys date, uid, ref and condition.
sub getAll {
my $self = shift;
my @entries = $self->_search(
'(&(objectClass=applicationProcess)(!(description={done}*)))');
my $result = {};
foreach my $entry (@entries) {
my @notifValues = $entry->get_value('description');
my $f = {};
foreach (@notifValues) {
my ( $k, $v ) = ( $_ =~ /\{(.*?)\}(.*)/smg );
$v = decodeLdapValue($v);
$f->{$k} = $v;
}
$result->{"$f->{date}#$f->{uid}#$f->{ref}"} = {
date => $f->{date},
uid => $f->{uid},
ref => $f->{ref},
cond => $f->{condition},
};
}
return $result;
}
## @method hashref getExisting()
# Return all notifications.
# @return hashref where keys are internal reference and values are hashref with
# keys date, uid, ref and condition.
sub getExisting {
my $self = shift;
my @entries = $self->_search('objectClass=applicationProcess');
my $result = {};
foreach my $entry (@entries) {
my @notifValues = $entry->get_value('description');
my $f = {};
foreach (@notifValues) {
my ( $k, $v ) = ( $_ =~ /\{(.*?)\}(.*)/smg );
$v = decodeLdapValue($v);
$f->{$k} = $v;
}
$result->{"$f->{date}#$f->{uid}#$f->{ref}"} = {
date => $f->{date},
uid => $f->{uid},
ref => $f->{ref},
cond => $f->{condition},
};
}
return $result;
}
## @method boolean delete(string myref)
# Mark a notification as done.
# @param $myref identifier returned by get() or getAll()
sub delete {
my ( $self, $myref ) = @_;
my ( $d, $u, $r );
unless ( ( $d, $u, $r ) = ( $myref =~ /^([^#]+)#(.+?)#(.+)$/ ) ) {
$self->logger->warn("Bad reference $myref");
return 0;
}
my @ts = localtime();
$ts[5] += 1900;
$ts[4]++;
return _modify(
$self,
'(&(objectClass=applicationProcess)'
. "(description={uid}$u)"
. "(description={ref}$r)"
. "(description={date}$d)"
. '(!(description={done}*)))',
"description",
"{done}$ts[5]-$ts[4]-$ts[3] $ts[2]:$ts[1]"
);
}
## @method boolean purge(string myref, boolean force)
# Purge notification (really delete record)
# @param $myref identifier returned by get or getAll
# @param $force force purge for not deleted session
# @return true if something was deleted
sub purge {
my ( $self, $myref, $force ) = @_;
my ( $d, $u, $r );
unless ( ( $d, $u, $r ) = ( $myref =~ /^([^#]+)#(.+?)#(.+)$/ ) ) {
$self->logger->warn("Bad reference $myref");
return 0;
}
my $clause = ( $force ? '' : '(description={done}*)' );
return $self->_delete( '(&(objectClass=applicationProcess)'
. "(description={uid}$u)"
. "(description={ref}$r)"
. "(description={date}$d)"
. "$clause)" );
}
# Insert a new notification
# @param date Date
# @param uid UID
# @param ref Reference of the notification
# @param condition Condition for the notification
# @param xml XML notification
# @return true if succeed
sub newNotif {
my ( $self, $date, $uid, $ref, $condition, $xml ) = @_;
my $fns = $self->conf->{fileNameSeparator};
$fns ||= '_';
my @t = split( /\D+/, $date );
$t[1]--;
eval {
timelocal( $t[5] || 0, $t[4] || 0, $t[3] || 0, $t[2], $t[1], $t[0] );
};
return ( 0, "Bad date" ) if ($@);
$date =~ s/-//g;
return ( 0, "Bad date" ) unless ( $date =~ /^\d{8}/ );
my $cn = "${date}${fns}${uid}${fns}" . encode_base64url( $ref, '' );
$cn .= "${fns}" . encode_base64url( $condition, '' ) if $condition;
my $fields =
$condition =~ /.+/
? {
"date" => "$date",
"uid" => "$uid",
"ref" => "$ref",
"xml" => "$xml",
"cond" => "$condition",
}
: {
"date" => "$date",
"uid" => "$uid",
"ref" => "$ref",
"xml" => "$xml",
};
return _store( $self, $cn, $fields );
}
## @method hashref getDone()
# Returns a list of notifications that have been done
# @return hashref where keys are internal reference and values are hashref with
# keys notified, uid and ref.
sub getDone {
my ($self) = @_;
my @entries = _search( $self,
'(&(objectClass=applicationProcess)(description={done}*))' );
my $result = {};
foreach my $entry (@entries) {
my @notifValues = $entry->get_value('description');
my $f = {};
foreach (@notifValues) {
my ( $k, $v ) = ( $_ =~ /\{(.*?)\}(.*)/smg );
$v = decodeLdapValue($v);
$f->{$k} = $v;
}
my @t = split( /\D+/, $f->{done} );
$t[1]--;
my $done =
eval { timelocal( $t[5], $t[4], $t[3], $t[2], $t[1], $t[0] ) };
if ($@) {
$self->logger->warn("Bad date: $f->{done}");
return {};
}
$result->{"$f->{date}#$f->{uid}#$f->{ref}"} =
{ notified => $done, uid => $f->{uid}, ref => $f->{ref}, };
}
# $ldap->unbind() && delete $self->{ldap};
return $result;
}
## @method object private _ldap()
# Return the ldap object (build it if needed).
# @param filter The LDAP filter to apply
# @return list of entries returned by the LDAP search (set of Net::LDAP::Entry)
sub _search {
my ( $self, $filter ) = @_;
my $ldap = _ldap($self);
my $search = $ldap->search(
base => $self->{ldapConfBase},
filter => "$filter",
scope => 'sub',
attrs => ['description'],
);
if ( $search->code ) {
$self->logger->error( "search error: " . $search->error() );
return ();
}
return $search->entries();
}
## @method object private _delete()
# Deletes the all entries found by the given LDAP filter
# @param filter The LDAP filter to apply
# @return 1 if operation success, else 0
sub _delete {
my ( $self, $filter ) = @_;
my @entries = _search( $self, "$filter" );
my $mesg = {};
foreach my $entry (@entries) {
$mesg = $self->{ldap}->delete( $entry->dn() );
$mesg->code && return 0;
}
# $ldap->unbind() && delete $self->{ldap};
return 1;
}
## @method object private _modify()
# add the given attribute value to all entries found by LDAP filter
# @param filter The LDAP filter to apply
# @param attr : name of the attribute to modify
# @param value : new value to add
# @return 1 if operation success, else 0
sub _modify {
my ( $self, $filter, $attr, $value ) = @_;
my @entries = _search( $self, "$filter" );
my $mesg = {};
foreach my $entry (@entries) {
$mesg =
$self->{ldap}
->modify( $entry->dn(), add => { "$attr" => "$value", }, );
$mesg->code && return 0;
}
# $ldap->unbind() && delete $self->{ldap};
return 1;
}
## @method object private _store()
# creates the notification defined by dn: cn=$cn,$ldapConfBase and $fields
# stored in the description attribute
# @param cn : cn value, used as a dn component
# @param fields : set of values to store in description attribute
# @return 1 if operation success, else 0
sub _store {
my ( $self, $cn, $fields ) = @_;
my $ldap = _ldap($self) or return 0;
my $notifName = "$cn";
my $notifDN = "cn=$notifName," . $self->{ldapConfBase};
# Store values as {key}value
my @notifValues;
foreach my $k ( keys %$fields ) {
my $v = encodeLdapValue( $fields->{$k} );
push @notifValues, "{$k}$v";
}
my $add = $ldap->add(
$notifDN,
attrs => [
objectClass => [ 'top', 'applicationProcess' ],
cn => $notifName,
description => \@notifValues,
]
);
if ( $add->code ) {
$self->logger->error( $add->error );
return 0;
}
#$ldap->unbind() && delete $self->{ldap};
return 1;
}
## @method object private encodeLdapValue()
# encode ldap value in utf8 (try to encode to latin1, and if it fails, encode to utf8)
# @param value value to encode
# @return value encoded in utf8
sub encodeLdapValue {
my $value = shift;
eval {
my $safevalue = $value;
Encode::from_to( $safevalue, "utf8", "iso-8859-1", Encode::FB_CROAK );
};
if ($@) {
Encode::from_to( $value, "iso-8859-1", "utf8", Encode::FB_CROAK );
}
return $value;
}
## @method object private decodeLdapValue()
# decode ldap value from utf8 to latin1
# @param value value to decode
# @return value decoded in latin1
sub decodeLdapValue {
my $value = shift;
Encode::from_to( $value, "utf8", "iso-8859-1", Encode::FB_CROAK );
return $value;
}
## @method object private _ldap()
# Return the ldap object (build it if needed).
# @return ldap handle object
sub _ldap {
my $self = shift;
return $self->{ldap} if ( $self->{ldap} );
# Parse servers configuration
my $useTls = 0;
my $tlsParam;
my @servers = ();
foreach my $server ( split /[\s,]+/, $self->ldapServer ) {
if ( $server =~ m{^ldap\+tls://([^/]+)/?\??(.*)$} ) {
$useTls = 1;
$server = $1;
$tlsParam = $2 || "";
}
else {
$useTls = 0;
}
push @servers, $server;
}
# Connect
my $ldap = Net::LDAP->new(
\@servers,
onerror => undef,
keepalive => 1,
( $self->ldapPort ? ( port => $self->ldapPort ) : () ),
( $self->ldapVerify ? ( verify => $self->ldapVerify ) : () ),
( $self->ldapCAFile ? ( cafile => $self->ldapCAFile ) : () ),
( $self->ldapCAPath ? ( capath => $self->ldapCAPath ) : () ),
);
unless ($ldap) {
use Data::Dumper;
die 'connexion failed: ' . $@;
}
elsif ( $Net::LDAP::VERSION < '0.64' ) {
# CentOS7 has a bug in which IO::Socket::SSL will return a broken
# socket when certificate validation fails. Net::LDAP does not catch
# it, and the process ends up crashing.
# As a precaution, make sure the underlying socket is doing fine:
if ( $ldap->socket->isa('IO::Socket::SSL')
and $ldap->socket->errstr < 0 )
{
die "SSL connection error: " . $ldap->socket->errstr;
}
}
# Start TLS if needed
if ($useTls) {
my %h = split( /[&=]/, $tlsParam );
$h{cafile} ||= $self->ldapCAFile if ( $self->ldapCAFile );
$h{capath} ||= $self->ldapCAPath if ( $self->ldapCAPath );
$h{verify} ||= $self->ldapVerify if ( $self->ldapVerify );
my $start_tls = $ldap->start_tls(%h);
if ( $start_tls->code ) {
die 'tls failed: ' . $start_tls->error;
}
}
# Bind with credentials
my $bind =
$ldap->bind( $self->ldapBindDN, password => $self->ldapBindPassword );
if ( $bind->code ) {
die 'bind failed: ' . $bind->error;
}
$self->{ldap} = $ldap;
return $ldap;
}
## @method string getIdentifier(string uid, string ref, string date)
# Get notification identifier
# @param $uid uid
# @param $ref ref
# @param $date date
# @return the notification identifier
sub getIdentifier {
my ( $self, $uid, $ref, $date ) = @_;
return $date . "#" . $uid . "#" . $ref;
}
1;