1# -*- mode: cperl; eval: (follow-mode); -*-
2#
3
4package App::Regather;
5
6use strict;
7use warnings;
8use diagnostics;
9
10use Carp;
11use File::Basename;
12use Getopt::Long qw(:config no_ignore_case gnu_getopt auto_version);
13use IPC::Open2;
14use List::Util   qw(uniqstr);
15
16use Net::LDAP;
17use Net::LDAP::LDIF;
18use Net::LDAP::Constant qw( LDAP_SYNC_REFRESH_ONLY
19			    LDAP_SYNC_REFRESH_AND_PERSIST
20			    LDAP_SUCCESS
21			    LDAP_SYNC_PRESENT
22			    LDAP_SYNC_ADD
23			    LDAP_SYNC_MODIFY
24			    LDAP_SYNC_DELETE
25			    LDAP_CONNECT_ERROR
26			    LDAP_OPERATIONS_ERROR
27			    LDAP_LOCAL_ERROR );
28use Net::LDAP::Control::SyncRequest;
29use Net::LDAP::Util qw(generalizedTime_to_time);
30
31use POSIX;
32use Pod::Usage   qw(pod2usage);
33use Sys::Syslog  qw(:standard :macros);
34use Template;
35
36use App::Regather::Config;
37use App::Regather::Logg;
38use App::Regather::Plugin;
39
40use constant SYNST => [ qw( LDAP_SYNC_PRESENT LDAP_SYNC_ADD LDAP_SYNC_MODIFY LDAP_SYNC_DELETE ) ];
41
42# my @DAEMONARGS = ($0, @ARGV);
43our $VERSION   = '0.81.03';
44
45sub new {
46  my $class = shift;
47  my $self =
48    bless {
49	   _progname => fileparse($0),
50	   _daemonargs => [$0, @ARGV],
51	   _opt   => {
52		      ch          => undef,
53		      cli         => undef,
54		      colors      => 0,
55		      config      => '/usr/local/etc/regather.conf',
56		      fg          => 0,
57		      force       => 0,
58		      plugin_list => 0,
59		      strict      => 0,
60		      ts_fmt      => "%a %F %T %Z (%z)",
61		      v           => 0,
62		     }
63	   }, $class;
64
65  GetOptions(
66	     'f|foreground' => \$self->{_opt}{fg},
67	     'F|force'      => \$self->{_opt}{force},
68	     'c|config=s'   => \$self->{_opt}{config},
69	     'colors'       => \$self->{_opt}{colors},
70	     'C|cli=s%'     => \$self->{_opt}{cli},
71	     'S|strict'     => \$self->{_opt}{strict},
72	     'config-help'  => \$self->{_opt}{ch},
73	     'plugin-list'  => \$self->{_opt}{plugin_list},
74	     'v+'           => \$self->{_opt}{v},
75	     'h'            => sub { pod2usage(-exitval => 0, -verbose => 2); exit 0 },
76	    );
77
78  $self->{_opt}{l} = new
79    App::Regather::Logg( prognam    => $self->{_progname},
80			 foreground => $self->{_opt}{fg},
81			 colors     => $self->{_opt}{colors} );
82
83  if ( $self->{_opt}{plugin_list} ) {
84    App::Regather::Plugin->new( 'list' )->run;
85    exit 0;
86  }
87
88  $self->{_opt}{cf} = new
89    App::Regather::Config ( filename => $self->{_opt}{config},
90			    logger   => $self->{_opt}{l},
91			    cli      => $self->{_opt}{cli},
92			    verbose  => $self->{_opt}{v} );
93
94  my $cf_mode = (stat($self->{_opt}{config}))[2] & 0777;
95  my $fm_msg;
96  if ( $cf_mode & 002 || $cf_mode & 006 ) {
97    $fm_msg = 'world';
98  } elsif ( $cf_mode & 020 || $cf_mode & 060) {
99    $fm_msg = 'group';
100  }
101  if ( defined $fm_msg ) {
102    $self->{_opt}{l}->cc( pr => 'err', fm => 'config file is accessible by ' . $fm_msg);
103    pod2usage(-exitval => 2, -sections => [ qw(USAGE) ]);
104    exit 1;
105  }
106
107  $self->{_opt}{last_forever} = 1;
108
109  # !!! TO CORRECT
110  if ( ! defined $self->{_opt}{cf} ) {
111    $self->l->cc( pr => 'err', fm => "do fix config file ..." );
112    pod2usage(-exitval => 2, -sections => [ qw(USAGE) ]);
113    exit 1;
114  }
115
116  if ( $self->{_opt}{ch} ) {
117    $self->{_opt}{cf}->config_help;
118    exit 1;
119  }
120
121  return $self;
122}
123
124sub progname { shift->{_progname} }
125
126sub progargs { return join(' ', @{shift->{_daemonargs}}); }
127
128sub cf { shift->{_opt}{cf} }
129
130sub l { shift->{_opt}{l} }
131
132sub o {
133  my ($self,$opt) = @_;
134  croak "unknown/undefined variable"
135    if ! exists $self->{_opt}{$opt};
136  return $self->{_opt}{$opt};
137}
138
139sub run {
140  my $self = shift;
141
142  $self->l->cc( pr => 'info', fm => "App::Regather::Logg initialized ..." ) if $self->o('v') > 1;
143
144  $self->l->cc( pr => 'info', fm => "%s: options provided from CLI:\n%s", ls => [ __PACKAGE__, $self->o('cli') ] )
145    if defined $self->o('cli') && keys( %{$self->o('cli')} ) && $self->o('v') > 1;
146
147  $self->l->set_m( $self->cf->getnode('log')->as_hash );
148  $self->l->set( notify       => [ $self->cf->get('core', 'notify') ] );
149  $self->l->set( notify_email => [ $self->cf->get('core', 'notify_email') ] );
150
151  $self->l->cc( pr => 'info', fm => "%s: Dry Run is set on, no file is to be changed\n" )
152    if $self->cf->get(qw(core dryrun));
153  $self->l->cc( pr => 'info', fm => "%s: Config::Parse object as hash:\n%s",
154	    ls => [ __PACKAGE__, $self->cf->as_hash ] ) if $self->o('v') > 3;
155  $self->l->cc( pr => 'info', fm => "%s: %s",
156	    ls => [ __PACKAGE__, $self->progargs ] );
157  $self->l->cc( pr => 'info', fm => "%s: %s v.%s is starting ...",
158	    ls => [ __PACKAGE__, $self->progname, $VERSION, ] );
159
160  @{$self->{_opt}{svc}} = grep { $self->cf->get('service', $_, 'skip') != 1 } $self->cf->names_of('service');
161
162  $self->daemonize if ! $self->o('fg');
163
164  our $s;
165  my  $tmp;
166  my  $cfgattrs = [];
167  my  $mesg;
168  my  @svc_map;
169
170  foreach my $i ( @{$self->{_opt}{svc}} ) {
171    foreach ( qw( s m ) ) {
172      if ( $self->cf->is_section('service', $i, 'map', $_) ) {
173	@svc_map = values( %{ $self->cf->getnode('service', $i, 'map', $_)->as_hash } );
174	# push @svc_map, $self->cf->getnode('service', $i, 'ctrl_attr');
175	$cfgattrs = [ @{$cfgattrs}, @svc_map, @{$self->cf->get('service', $i, 'ctrl_attr')} ];
176      } else {
177	@svc_map = ();
178      }
179    }
180
181    push @{$cfgattrs}, '*'
182      if $self->cf->get('service', $i, 'all_attr') != 0;
183
184    $self->l->cc( pr => 'warning', ls => [ __PACKAGE__, $i, ],
185		  fm => "%s: no LDAP attribute to process is mapped for service `%s`" )
186      if $self->cf->get('service', $i, 'all_attr') == 0 && scalar @svc_map == 0;
187
188  }
189
190  @{$tmp} = sort @{[ @{$cfgattrs}, qw( associatedDomain
191				       authorizedService
192				       description
193				       entryUUID
194				       entryCSN
195				       createTimestamp
196				       creatorsName
197				       modifiersName
198				       modifyTimestamp ) ]};
199  @{$cfgattrs} = uniqstr @{$tmp};
200
201  #
202  ## -=== MAIN LOOP =====================================================-
203  #
204
205  my $ldap_opt      = $self->cf->getnode(qw(ldap opt))->as_hash;
206  my $uri           = delete $ldap_opt->{uri};
207  while ( $self->o('last_forever') ) {
208    if ( $self->cf->is_set(qw(core altroot)) ) {
209      chdir($self->cf->get(qw(core altroot))) || do {
210	$self->l->cc( pr => 'err', fm => "%s: main: unable to chdir to %s",
211		  ls => [ __PACKAGE__, $self->cf->get(qw(core altroot)) ] );
212	exit 1;
213      };
214    }
215
216    $self->{_opt}{ldap} = Net::LDAP->new( $uri,
217			    @{[ map { $_ => $ldap_opt->{$_} } %$ldap_opt ]} )
218      || do {
219	$self->l->cc( pr => 'err', fm => "%s: Unable to connect to %s; error: %s",
220		  ls => [ __PACKAGE__, $uri, $! ] );
221	if ( $self->o('strict') ) {
222	  exit LDAP_CONNECT_ERROR;
223	} else {
224	  next;
225	}
226      };
227
228    my $start_tls_options = $self->cf->getnode(qw(ldap ssl))->as_hash if $self->cf->is_section(qw(ldap ssl));
229    if ( exists $start_tls_options->{ssl} && $start_tls_options->{ssl} eq 'start_tls' ) {
230      delete $start_tls_options->{ssl};
231      eval {
232	$mesg =
233	  $self->o('ldap')->start_tls( @{[ map { $_ => $start_tls_options->{$_} } %$start_tls_options ]} );
234      };
235      if ( $@ ) {
236	$self->l->cc( pr => 'err', fm => "%s: TLS negotiation failed: %s", ls => [ __PACKAGE__, $! ] );
237	if ( $self->o('strict') ) {
238	  exit LDAP_CONNECT_ERROR;
239	} else {
240	  next;
241	}
242      } else {
243	$self->l->cc( pr => 'info', fm => "%s: TLS negotiation succeeded" ) if $self->o('v') > 1;
244      }
245    }
246
247    my $bind = $self->cf->getnode(qw(ldap bnd))->as_hash if $self->cf->is_section(qw(ldap bnd));
248    if ( ref($bind) eq 'HASH' ) {
249      if ( exists $bind->{dn} ) {
250	my @bind_options;
251	push @bind_options, delete $bind->{dn};
252	while ( my($k, $v) = each %{$bind} ) {
253	  push @bind_options, $k => $v;
254	}
255	$mesg = $self->o('ldap')->bind( @bind_options );
256	if ( $mesg->code ) {
257	  ####### !!!!!!! TODO: to implement exponential delay on error sending to awoid log file/notify
258	  ####### !!!!!!! queue overflow
259	  $self->l->cc( pr => 'err', fm => "%s: bind error: %s", ls => [ __PACKAGE__, $mesg->error ] );
260	  if ( $self->o('strict') ) {
261	    exit $mesg->code;
262	  } else {
263	    next;
264	  }
265	}
266      }
267    }
268
269    $self->{_opt}{req} = Net::LDAP::Control::SyncRequest->new( mode     => LDAP_SYNC_REFRESH_AND_PERSIST,
270						 critical => 1,
271						 cookie   => undef, );
272
273    $mesg = $self->o('ldap')->search( base     => $self->cf->get(qw(ldap srch base)),
274				      scope    => $self->cf->get(qw(ldap srch scope)),
275				      control  => [ $self->o('req') ],
276				      callback => sub {$self->ldap_search_callback(@_)},
277				      filter   => $self->cf->get(qw(ldap srch filter)),
278				      attrs    => $cfgattrs,
279				      sizelimit=> $self->cf->get(qw(ldap srch sizelimit)),
280				      timelimit=> $self->cf->get(qw(ldap srch timelimit)),
281			 );
282    if ( $mesg->code ) {
283      $self->l->cc( pr => 'err',
284		fm => "%s: LDAP search ERROR...\n% 13s%s\n% 13s%s\n% 13s%s\n% 13s%s\n\n",
285		ls => [ __PACKAGE__,
286			'base: ',         $self->cf->get(qw(ldap srch base)),
287			'scope: ',        $self->cf->get(qw(ldap srch scope)),
288			'filter: ',       $self->cf->get(qw(ldap srch filter)),
289			'attrs: ',        join("\n", @{$cfgattrs}) ] );
290      $self->l->cc_ldap_err( mesg => $mesg );
291      exit $mesg->code if $self->o('strict');
292    } else {
293      $self->l->cc( pr => 'info',
294		fm => "%s: LDAP search:\n% 13s%s\n% 13s%s\n% 13s%s\n% 13s%s\n\n",
295		ls => [ __PACKAGE__,
296			'base: ',   $self->cf->get(qw(ldap srch base)),
297			'scope: ',  $self->cf->get(qw(ldap srch scope)),
298			'filter: ', $self->cf->get(qw(ldap srch filter)),
299			'attrs: ',  join("\n", @{$cfgattrs}) ] ) if $self->o('v') > 2;
300    }
301  }
302
303  $mesg = $self->o('ldap')->unbind;
304  if ( $mesg->code ) {
305    $self->l->cc_ldap_err( mesg => $mesg );
306    exit $mesg->code;
307  }
308
309  closelog();
310
311}
312
313#
314## ===================================================================
315#
316
317sub daemonize {
318  my $self = shift;
319
320  my ( $pid, $fh, $pp, $orphaned_pid_mtime );
321  if ( -e $self->cf->get(qw(core pid_file)) ) {
322    open( $fh, "<", $self->cf->get(qw(core pid_file))) || do {
323      die "Can't open $self->cf->get(qw(core pid_file)) for reading: $!";
324      exit 1;
325    };
326    $pid = <$fh>;
327    close($fh) || do {
328      print "close $self->cf->get(qw(core pid_file)) (opened for reading) failed: $!\n\n";
329      exit 1;
330    };
331
332    if ( kill(0, $pid) ) {
333      print "Doing nothing\npidfile $self->cf->get(qw(core pid_file)) of the proces with pid $pid, exists and the very process is alive\n\n";
334      exit 1;
335    }
336
337    $orphaned_pid_mtime = strftime( $self->o('ts_fmt'), localtime( (stat( $self->cf->get(qw(core pid_file)) ))[9] ));
338    if ( unlink $self->cf->get(qw(core pid_file)) ) {
339      $self->l->cc( pr => 'debug', fm => "%s: orphaned %s was removed",
340		ls => [ __PACKAGE__, $self->cf->get(qw(core pid_file)) ] )
341	if $self->o('v') > 0;
342    } else {
343      $self->l->cc( pr => 'err', fm => "%s: orphaned %s (mtime: %s) was not removed: %s",
344		ls => [ __PACKAGE__, $self->cf->get(qw(core pid_file)), $orphaned_pid_mtime, $! ] );
345      exit 2;
346    }
347
348    undef $pid;
349  }
350
351  $pid = fork();
352  die "fork went wrong: $!\n\n" unless defined $pid;
353  exit(0) if $pid != 0;
354
355  setsid || do { print "setsid went wrong: $!\n\n"; exit 1; };
356
357  open( $pp, ">", $self->cf->get(qw(core pid_file))) || do {
358    print "Can't open $self->cf->get(qw(core pid_file)) for writing: $!"; exit 1; };
359  print $pp "$$";
360  close( $pp ) || do {
361    print "close $self->cf->get(qw(core pid_file)) (opened for writing), failed: $!\n\n"; exit 1; };
362
363  if ( $self->o('v') > 1 ) {
364    open (STDIN,  "</dev/null") || do { print "Can't redirect /dev/null to STDIN\n\n";  exit 1; };
365    open (STDOUT, ">/dev/null") || do { print "Can't redirect STDOUT to /dev/null\n\n"; exit 1; };
366    open (STDERR, ">&STDOUT")   || do { print "Can't redirect STDERR to STDOUT\n\n";    exit 1; };
367  }
368
369  $SIG{HUP}  =
370    sub { my $sig = @_;
371	  $self->l->cc( pr => 'warning', fm => "%s: SIG %s received, restarting", ls => [ __PACKAGE__, $sig ] );
372	  exec('perl', @{$self->o('_daemonargs')}); };
373  $SIG{INT} = $SIG{QUIT} = $SIG{ABRT} = $SIG{TERM} =
374    sub { my $sig = @_;
375	  $self->l->cc( pr => 'warning', fm => "%s:  SIG %s received, exiting", ls => [ __PACKAGE__, $sig ] );
376	  $self->{_opt}{last_forever} = 0;
377	};
378  $SIG{PIPE} = 'ignore';
379  $SIG{USR1} =
380    sub { my $sig = @_;
381	  $self->l->cc( pr => 'warning', fm => "%s: SIG %s received, doing nothing" ), ls => [ __PACKAGE__, $sig ] };
382
383  if ( $self->cf->is_set(qw(core uid)) && $self->cf->is_set(qw(core gid)) ) {
384    setgid ( $self->cf->get(qw(core gid_number)) ) || do { print "setgid went wrong: $!\n\n"; exit 1; };
385    setuid ( $self->cf->get(qw(core uid_number)) ) || do { print "setuid went wrong: $!\n\n"; exit 1; };
386  }
387
388  $self->l->cc( pr => 'info', fm => "%s: %s v.%s is started.", ls => [ __PACKAGE__, $self->progname, $VERSION ] );
389}
390
391sub ldap_search_callback {
392  my ( $self, $msg, $obj ) = @_;
393
394
395  my @controls = $msg->control;
396  my $syncstate = scalar @controls ? $controls[0] : undef;
397
398  my ( $s, $st, $mesg, $entry, @entries, $ldif, $map,
399       $out_file_pfx_old,
400       $tmp_debug_msg,
401       $rdn, $rdn_old, $rdn_re,
402       $pp, $chin, $chou, $chst, $cher, $email, $email_body );
403
404  ######## !! not needed ?
405  my $out_file_old;
406
407  $self->l->cc( pr => 'debug', fm => "%s: syncstate: %s", ls => [ __PACKAGE__, $syncstate ] )
408    if $self->o('v') > 5;
409  $self->l->cc( pr => 'debug', fm => "%s: object: %s", ls => [ __PACKAGE__, $obj ] ) if $self->o('v') > 5;
410
411  if ( defined $obj && $obj->isa('Net::LDAP::Entry') ) {
412    $rdn = ( split(/=/, ( split(/,/, $obj->dn) )[0]) )[0];
413    if ( defined $syncstate && $syncstate->isa('Net::LDAP::Control::SyncState') ) {
414      $self->l->cc( pr => 'debug', fm => "%s: SYNCSTATE:\n%s:", ls => [ __PACKAGE__, $syncstate ] )
415	if $self->o('v') > 4;
416      $st = $syncstate->state;
417      my %reqmod;
418      $self->l->cc( fm => "%s: received control %s: dn: %s", ls => [ __PACKAGE__, SYNST->[$st], $obj->dn ] );
419
420      #######################################################################
421      ####### --- PRELIMINARY STUFF ----------------------------->>>>>>>>> 0
422      #######################################################################
423
424      ### LDAP_SYNC_DELETE arrives for both cases, object deletetion and attribute
425      ### deletion and in both cases Net::LDAP::Entry provided contains only DN,
426      ### so, we need to "re-construct" it for further processing
427      if ( $st == LDAP_SYNC_DELETE ) {
428	$mesg = $self->o('ldap')->search( base     => $self->cf->get(qw(ldap srch log_base)),
429			       scope    => 'sub',
430			       sizelimit=> $self->cf->get(qw(ldap srch sizelimit)),
431			       timelimit=> $self->cf->get(qw(ldap srch timelimit)),
432			       filter   => '(reqDN=' . $obj->dn . ')', );
433	if ( $mesg->code ) {
434	  $self->l->cc( pr => 'err', nt => 1,
435		    fm => "%s: LDAP accesslog search on %s, error:\n% 13s%s\n% 13s%s\n% 13s%s\n\n",
436		    ls => [ __PACKAGE__, SYNST->[$st],
437			    'base: ',   $self->cf->get(qw(ldap srch log_base)),
438			    'scope: ',  'sub',
439			    'filter: ', '(reqDN=' . $obj->dn . ')' ] );
440	  $self->l->cc_ldap_err( mesg => $mesg );
441	  # exit $mesg->code; # !!! NEED TO DECIDE WHAT TO DO
442	} else {
443	  $entry = pop @{[$mesg->sorted]};
444
445	  if ( ! $entry->isa('Net::LDAP::Entry') ) {
446	    $self->l->cc( pr => 'err', nt => 1,
447		      fm => "%s: LDAP accesslog search on %s, returned no result:\n% 13s%s\n% 13s%s\n% 13s%s\n\n",
448		      ls => [ __PACKAGE__, SYNST->[$st],
449			      'base: ',   $self->cf->get(qw(ldap srch log_base)),
450			      'scope: ',  'sub',
451			      'filter: ', '(reqDN=' . $obj->dn . ')' ] );
452	    return;
453	  } elsif ( $entry->get_value('reqType') eq 'delete' ) {
454	    my $reqold = 'dn: ' . $obj->dn;
455	    foreach ( @{$entry->get_value('reqOld', asref => 1)} ) {
456	      s/^(.*;binary:) .*$/$1: c3R1Yg==/agis;
457	      $reqold .= "\n" . $_;
458	    }
459	    my ( $file, @err );
460	    open( $file, "<", \$reqold) ||
461	      $self->l->cc( pr => 'err',
462			fm => "%s: Cannot open data from variable to read ldif: %s",
463			ls => [ __PACKAGE__, $! ] );
464	    $ldif = Net::LDAP::LDIF->new( $file, "r", onerror => 'warn' );
465	    while ( not $ldif->eof ) {
466	      $entry = $ldif->read_entry;
467	      $self->l->cc( pr => 'err', fm => "%s: Reading LDIF error: %s",
468			ls => [ __PACKAGE__, $ldif->error ] ) if $ldif->error;
469	    }
470	    $obj = $entry;
471	    $ldif->done;
472	  } elsif ( $entry->get_value('reqType') eq 'modify' ) {
473	    ### here we re-assemble $obj to have all attributes before deletion and since
474	    ### after that it'll has ctrl_attr but reqType=delete, it'll go to $st == LDAP_SYNC_DELETE
475	    %reqmod = map  { substr($_, 0, -2) => 1 } grep { /^(.*):-$/g }
476	      @{$entry->get_value('reqMod', asref => 1)};
477
478	    $mesg = $self->o('ldap')->search( base   => $obj->dn,
479				   scope  => 'base',
480				   filter => '(objectClass=*)', );
481	    if ( $mesg->code ) {
482	      $self->l->cc( pr => 'err', nt => 1,
483			fm => "%s: LDAP search %s %s error:\n% 13s%s\n% 13s%s\n% 13s%s\n\n",
484			ls => [ __PACKAGE__, SYNST->[$st], 'reqType=modify',
485				'base: ',     $self->cf->get(qw(ldap srch log_base)),
486				'scope: ',    'sub',
487				'filter: ',   '(reqDN=' . $obj->dn . ')' ] );
488	      $self->l->cc_ldap_err( mesg => $mesg );
489	      # exit $mesg->code; # !!! NEED TO DECIDE WHAT TO DO
490	    } else {
491	      $obj = $mesg->entry(0);
492	      $obj->add( map { $_ => $reqmod{$_} } keys %reqmod );
493	      # $obj->add( $_ => $reqmod{$_} ) foreach ( keys %reqmod );
494	    }
495	    $self->l->cc( pr => 'debug', fm => "%s: %s reqType=modify reqMod: %s",
496		      ls => [ __PACKAGE__, SYNST->[$st], \%reqmod ] )	if $self->o('v') > 3;
497	  }
498	}
499      } elsif ( $st == LDAP_SYNC_MODIFY ) {
500	$mesg = $self->o('ldap')->search( base     => $self->cf->get(qw(ldap srch log_base)),
501			       scope    => 'sub',
502			       sizelimit=> $self->cf->get(qw(ldap srch sizelimit)),
503			       timelimit=> $self->cf->get(qw(ldap srch timelimit)),
504			       filter   => '(reqDN=' . $obj->dn . ')', );
505	if ( $mesg->code ) {
506	  $self->l->cc( pr => 'err', nt => 1,
507		    fm => "%s: LDAP accesslog search on %s, error:\n% 13s%s\n% 13s%s\n% 13s%s\n\n",
508		    ls => [ __PACKAGE__, SYNST->[$st], nt => 1,
509			    'base: ',   $self->cf->get(qw(ldap srch log_base)),
510			    'scope: ',  'sub',
511			    'filter: ', '(reqDN=' . $obj->dn . ')' ] );
512	  $self->l->cc_ldap_err( mesg => $mesg );
513	} else {
514	  if ( $mesg->count > 0 ) {
515	    ### modified object has accesslog records when it was add/modify/delete
516	    ### before, as well as ModRDN ... so, we need to be sure, there is no accesslog
517	    ### object with reqNewRDN=<$obj->dn RDN> close to the processing time of this $obj
518
519	    ### NEED TO BE FINISHED
520
521	  } elsif ( $mesg->count == 0 ) {
522	    ### modified object has no accesslog records when it was ModRDN-ed so, we search
523	    ### for accesslog object with reqNewRDN=<$obj->dn RDN> to know old object RDN to use
524	    ### it further for $out_file
525	    $mesg = $self->o('ldap')->search( base     => $self->cf->get(qw(ldap srch log_base)),
526				   scope    => 'sub',
527				   sizelimit=> $self->cf->get(qw(ldap srch sizelimit)),
528				   timelimit=> $self->cf->get(qw(ldap srch timelimit)),
529				   filter   => '(reqNewRDN=' . (split(/,/, $obj->dn))[0] . ')', );
530	    if ( $mesg->code ) {
531	      $self->l->cc( pr => 'err', nt => 1,
532			fm => "%s: LDAP accesslog search on %s, error:\n% 13s%s\n% 13s%s\n% 13s%s\n\n",
533			ls => [ __PACKAGE__, SYNST->[$st],
534				'base: ',   $self->cf->get(qw(ldap srch log_base)),
535				'scope: ',  'sub',
536				'filter: ', '(reqNewRDN=' . (split(/,/, $obj->dn))[0] . ')' ] );
537	      $self->l->cc_ldap_err( mesg => $mesg );
538	      # exit $mesg->code; # !!! NEED TO DECIDE WHAT TO DO
539	    } else {
540	      ### here we pick last reqNewRDN entry up, to find the latest UUID for entries
541	      ### with same DN if the object was added/deleted/ModRDN-ed several times
542	      @entries = $mesg->sorted;
543	      $entry = pop @entries;
544	      if ( defined $entry ) {
545		$rdn_re = qr/^$rdn: .*$/;
546		###### !!! NEDD FIX
547		### slapd.log-20210510-regather-fails-on-rdn
548		###
549		### here we're searching master db log, and the record is absent there, while
550		### it is still present (have no idea why) in local db log ...
551		###
552		### Can't use an undefined value as an ARRAY reference at /usr/local/lib/perl5/site_perl/App/Regather.pm line 546.
553		### BEGIN failed--compilation aborted at /usr/local/bin/regather line 10 (#1)
554		###     (F) A value used as either a hard reference or a symbolic reference must
555		###     be a defined value.  This helps to delurk some insidious errors.
556		### Uncaught exception from user code:
557		###         Can't use an undefined value as an ARRAY reference at /usr/local/lib/perl5/site_perl/App/Regather.pm line 546.
558		###         BEGIN failed--compilation aborted at /usr/local/bin/regather line 10.
559		###### !!! NEED FIX
560		if ( $entry->exists('reqOld') {
561		  foreach ( @{$entry->get_value('reqOld', asref => 1)} ) {
562		    $rdn_old = (split(/: /, $_))[1] if /$rdn_re/;
563		  }
564		}
565		### now we reconstruct original object
566		$mesg = $self->o('ldap')->search( base     => $self->cf->get(qw(ldap srch log_base)),
567				       scope    => 'sub',
568				       sizelimit=> $self->cf->get(qw(ldap srch sizelimit)),
569				       timelimit=> $self->cf->get(qw(ldap srch timelimit)),
570				       filter   => sprintf("(reqEntryUUID=%s)",
571							   $entry->get_value('reqEntryUUID')) );
572		if ( $mesg->code ) {
573		  $self->l->cc( pr => 'err', nt => 1,
574			    fm => "%s: LDAP accesslog search on %s, error:\n% 13s%s\n% 13s%s\n% 13s%s\n\n",
575			    ls => [ __PACKAGE__, SYNST->[$st],
576				    'base: ',   $self->cf->get(qw(ldap srch log_base)),
577				    'scope: ',  'sub',
578				    'filter: ', sprintf("(reqEntryUUID=%s)",
579							$entry->get_value('reqEntryUUID') ) ] );
580		  $self->l->cc_ldap_err( mesg => $mesg );
581		  # exit $mesg->code; # !!! NEED TO DECIDE WHAT TO DO
582		} else {
583		  @entries = $mesg->sorted;
584		  if ( $entries[0]->get_value('reqType') eq 'add' ) {
585		    ### here we re-assemble $obj to have all attributes on its creation except RDN,
586		    ### which we'll set from next to the last element and since after that it'll has
587		    ### ctrl_attr but reqType=add, it'll go to $st == LDAP_SYNC_DELETE
588		    $obj->add( map { /^(.*):\+ (.*)$/g } @{$entry->get_value('reqMod', asref => 1)} );
589		    $obj->replace( $rdn => $entries[scalar(@entries) - 2]->get_value($rdn) );
590		  } else {
591		    $self->l->cc( pr => 'err', nt => 1,
592			      fm => "%s: %s object (before ModRDN) to delete not found! accesslog reqType=add object nod found\n\nobject reqEntryUUID=%s should be processed manually",
593			      ls => [ __PACKAGE__, SYNST->[$st], $entry->get_value('reqEntryUUID') ] );
594		  }
595		}
596	      } else {
597		$self->l->cc( pr => 'err', nt => 1, ls => [ __PACKAGE__, SYNST->[$st] ],
598			  fm => "%s: LDAP accesslog search on %s object returned no result\n\n" );
599	      }
600	    }
601	  }
602	}
603      }
604
605      ### picking up a service, the $obj relates to
606      my $is_ctrl_attr;
607      my $ctrl_srv_re;
608      foreach ( @{$self->{_opt}{svc}} ) {
609	$is_ctrl_attr = 0;
610	foreach my $ctrl_attr ( @{$self->cf->get('service', $_, 'ctrl_attr')} ) {
611	  if ( $obj->exists( $ctrl_attr ) ) {
612	    $is_ctrl_attr++;
613	  } else {
614	    $is_ctrl_attr--;
615	  }
616	}
617	$ctrl_srv_re = $self->cf->get('service', $_, 'ctrl_srv_re');
618	if ( $is_ctrl_attr > 0 && $obj->dn =~ qr/$ctrl_srv_re/ &&
619	     $is_ctrl_attr == scalar( @{$self->cf->get('service', $_, 'ctrl_attr')} ) ) {
620	  $s = $_;
621	}
622      }
623
624      if ( ! defined $s ) {
625	$self->l->cc( pr => 'warning', ls => [ __PACKAGE__, $obj->dn, SYNST->[$st] ],
626		  fm => "%s: dn: %s is not configured to be processed on control: %s" )
627	  if $self->o('v') > 2;
628	return;
629      }
630
631      #######################################################################
632      ####### --------------------------------------------------->>>>>>>>> 1
633      #######################################################################
634      if ( $st == LDAP_SYNC_ADD || $st == LDAP_SYNC_MODIFY ) {
635
636	# App::Regather::Plugin->new( 'args', { log    => $self->log,
637	# 				 params => [ 1, 2, 3]} )->run;
638	foreach my $svc ( @{$self->cf->get('service', $s, 'plugin')} ) {
639	  App::Regather::Plugin->new( $svc, {
640					cf           => $self->cf,
641					force        => $self->o('force'),
642					log          => $self->l,
643					obj          => $obj,
644					out_file_old => $out_file_old,
645					prog         => sprintf("%s v.%s", $self->progname, $VERSION),
646					rdn          => $rdn,
647					s            => $s,
648					st           => $st,
649					ts_fmt       => $self->o('ts_fmt'),
650					v            => $self->o('v'),
651				       } )->ldap_sync_add_modify;
652	}
653
654	#######################################################################
655	####### --------------------------------------------------->>>>>>>>> 2
656	#######################################################################
657      } elsif ( $st == LDAP_SYNC_DELETE ) {
658
659	foreach my $svc ( @{$self->cf->get('service', $s, 'plugin')} ) {
660	  App::Regather::Plugin->new( $svc, {
661					cf           => $self->cf,
662					force        => $self->o('force'),
663					log          => $self->l,
664					obj          => $obj,
665					out_file_old => $out_file_old,
666					prog         => sprintf("%s v.%s", $self->progname, $VERSION),
667					rdn          => $rdn,
668					s            => $s,
669					st           => $st,
670					ts_fmt       => $self->o('ts_fmt'),
671					v            => $self->o('v'),
672				       } )->ldap_sync_delete;
673	}
674
675      }
676    } elsif ( defined $syncstate && $syncstate->isa('Net::LDAP::Control::SyncDone') ) {
677      $self->l->cc( pr => 'debug', fm => "%s: Received SYNC DONE CONTROL" ) if $self->o('v') > 1;
678    } elsif ( ! defined $syncstate ) {
679      $self->l->cc( pr => 'warning', fm => "%s: LDAP entry without Sync State control" ) if $self->o('v') > 1;
680    }
681
682    $self->o('req')->cookie($syncstate->cookie) if $syncstate->cookie;
683
684  } elsif ( defined $obj && $obj->isa('Net::LDAP::Intermediate') ) {
685    $self->l->cc( pr => 'debug', fm => "%s: Received Net::LDAP::Intermediate\n%s", ls => [ __PACKAGE__, $obj ] )
686      if $self->o('v') > 3;
687    $self->o('req')->cookie($obj->{'asn'}->{'refreshDelete'}->{'cookie'});
688  } elsif ( defined $obj && $obj->isa('Net::LDAP::Reference') ) {
689    $self->l->cc( pr => 'debug', fm => "%s: Received Net::LDAP::Reference\n%s", ls => [ __PACKAGE__, $obj ] )
690      if $self->o('v') > 3;
691    return;
692  } else {
693    return;
694  }
695}
696
6971;
698