#!/usr/bin/perl
#=============================================================================
# LemonLDAP::NG session conversion tool
#
# This script lets an administrator migrate existing sessions from one backend
# to another. It is mostly useful when run on persistant sessions, but it can be
# useful in some other cases too, such as OIDC Offline sessions
#
# This is part of LemonLDAP::NG product, released under GPL
#=============================================================================

use Lemonldap::NG::Common::Apache::Session;
use Lemonldap::NG::Common::Session;
use Config::IniFiles;
use strict;
use Getopt::Long;
use Pod::Usage;

our $VERSION = "2.0.12";

# Options
# -d: debug mode
# -c: configuration file
# -r: rename attributes
# -i: ignore errors
# -x: exclude attributes

my $debug;
my $config_file;
my $ignore_errors;
my %rename;
my @exclude;
my $help;
my $nb_converted = 0;
my $nb_error     = 0;

GetOptions(
    'help|?'          => \$help,
    'debug|d'         => \$debug,
    'config|c=s'      => \$config_file,
    'ignore-errors|i' => \$ignore_errors,
    'rename|r=s'      => \%rename,
    'exclude|x=s'     => \@exclude,
) or pod2usage(2);
pod2usage(
    -exitval  => 1,
    -verbose  => 99,
    -sections => "SYNOPSIS|OPTIONS|CONFIGURATION FILE FORMAT"
) if $help;

unless ($config_file) {
    pod2usage(
        -exitval  => 2,
        -verbose  => 99,
        -message  => "You must provide the -c option\n",
        -sections => "SYNOPSIS|OPTIONS|CONFIGURATION FILE FORMAT"
    );
}

my $inicfg =
  Config::IniFiles->new( -file => $config_file, -allowcontinue => 1 );
my $cfg = {};

die "Could not read configuration file" unless $inicfg;

for my $section (qw/sessions_from sessions_to/) {
    die "Could not find section $section in configuration file $config_file"
      unless $inicfg->SectionExists($section);

    # Load section parameters
    my $r;
    foreach ( $inicfg->Parameters($section) ) {
        $r->{$_} = $inicfg->val( $section, $_ );

        # Remove spaces before and after value (#1488)
        $r->{$_} =~ s/^\s*(.+?)\s*/$1/;
        if ( $r->{$_} =~ /^[{\[].*[}\]]$/ || $r->{$_} =~ /^sub\s*{.*}$/ ) {
            eval "\$r->{$_} = $r->{$_}";
            if ($@) {
                die "Error evaluating $section/$_: $@";
            }
        }
    }
    $cfg->{$section} = $r;
}

my $backendFrom;
my $backendTo;
my @sessionKindOnly;
if ( $cfg->{sessions_from}->{sessionKind} ) {
    @sessionKindOnly = split /\W+/, $cfg->{sessions_from}->{sessionKind};
}

if ( $cfg->{sessions_from}->{storageModule} ) {
    $backendFrom = $cfg->{sessions_from}->{storageModuleOptions};
    $backendFrom->{backend} = $cfg->{sessions_from}->{storageModule};
}
else {
    die
      "[sessions_from] configuration section does not declare a storageModule";
}

if ( $cfg->{sessions_to}->{storageModule} ) {
    $backendTo = $cfg->{sessions_to}->{storageModuleOptions};
    $backendTo->{backend} = $cfg->{sessions_to}->{storageModule};
}
else {
    die "[sessions_to] configuration section does not declare a storageModule";
}

Lemonldap::NG::Common::Apache::Session->get_key_from_all_sessions(
    $backendFrom,
    sub {
        my $entry = shift;
        my $id    = shift;

        # If filtering sessionKind
        if (@sessionKindOnly) {

            unless ( grep { $_ eq $entry->{_session_kind} } @sessionKindOnly ) {
                print "Ignoring session $id with type "
                  . $entry->{_session_kind} . "\n"
                  if $debug;
                return undef;
            }
        }

        if (%rename) {
            for my $oldkey ( keys %rename ) {
                my $newkey = $rename{$oldkey};
                if ( $newkey and $entry->{$oldkey} ) {
                    print "Renaming $oldkey to $newkey in session $id\n"
                      if $debug;
                    $entry->{$newkey} = delete $entry->{$oldkey};
                }
            }
        }

        if (@exclude) {
            for my $excludekey (@exclude) {
                if ( $entry->{$excludekey} ) {
                    print "Exclude $excludekey in session $id\n"
                      if $debug;
                    delete $entry->{$excludekey};
                }
            }
        }

        print "Processing session $id\n" if $debug;
        my $s = Lemonldap::NG::Common::Session->new( {
                storageModule        => $backendTo->{backend},
                storageModuleOptions => $backendTo,
                id                   => $id,
                info                 => $entry,
                force                => 1,
            }
        );

        if ( $s->error ) {
            die "Error encountered on session $id" unless $ignore_errors;
            $nb_error += 1;
            print "Error converting session $id : " . $s->error . "\n";
        }
        else {
            print "Session $id successfully converted\n" if $debug;
            $nb_converted += 1;
        }
    }
);

print "$nb_converted sessions have been converted\n";

print "$nb_error errors encountered during conversion\n" if $nb_error;

my $exit = $nb_error ? 1 : 0;
exit $exit;

__END__

=head1 NAME

=encoding utf8

convertSessions - A tool to convert Lemonldap::NG sessions between storage backends.

=head1 SYNOPSIS

  convertSession [-di] [-r oldkey=newkey ] -c parameters.ini


=head1 DESCRIPTION

convertConfig is a command line tool to migrate all sessions stored
in a source backend (sessions_from), into a new backend (sessions_to).

It requires a special configuration file in which you must list the source
and destination backend modules and parameters.

Sessions will not be deleted from the source backend. Existing sessions in the
destination backend will be kept, unless they have the same session ID as a
session in the source backend. In that case, the source will overwrite the
destination.

=head1 OPTIONS

=over

=item B<--config>,B<-c>

Specify configuration file

=item B<--debug>,B<-d>

Turns on debugging information

=item B<--ignore-errors>,B<-i>

Skip to the next session if converting a session fails

=item B<--rename oldkey=newkey>,B<-r oldkey=newkey>

Rename key names when migrating from one backend to the next.

This option can be specified multiple times

=back

=head1 CONFIGURATION FILE FORMAT

The configuration file needs two sections to describe the source and destination backends

Here is an example

    [sessions_from]
    storageModule = Apache::Session::File
    storageModuleOptions = {	\
          'Directory' => '/var/lib/lemonldap-ng/sessions', 	\
          'LockDirectory' => '/var/lib/lemonldap-ng/sessions/lock', \
    }
    # Only migrate some session types
    # sessionKind = Persistent, SSO
    
    [sessions_to]
    storageModule = Apache::Session::Browseable::Postgres
    storageModuleOptions = {	\
    	'DataSource' => 'DBI:Pg:database=lemonldapdb;host=pg.example.com', \
    	'UserName' => 'lemonldaplogin', \
    	'Password' => 'lemonldappw', \
    	'Commit' => 1, \
    	'Index' => 'ipAddr _whatToTrace user', \
    	'TableName' => 'sessions', \
    }


The C<sessionKind> parameter may be used to filter only some session types.

Thanks to this, you can use this script to migrate from one database holding 
all your sessions to separate tables from each session type.


=head1 SEE ALSO

L<http://lemonldap-ng.org/>

=head1 AUTHORS

=over

=item Maxime Besson, E<lt>maxime.besson@worteks.comE<gt>

=back

=head1 BUG REPORT

Use OW2 system to report bug or ask for features:
L<https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues>

=head1 DOWNLOAD

Lemonldap::NG is available at
L<https://lemonldap-ng.org/download>

