#!/usr/bin/perl
#
# Migrate the relevant parts of the Debian Edu LDAP database from
# Squeeze to Wheezy.
#
# Extract users and groups from the slapcad output to insert with
# slapadd.  It must be run just after installation of the main server.

sub usage {
    my $exitcode = shift;
    print <<'EOF';
Usage: $0

Migrate LDAP information from a Debian Edu Squeeze main server to a
Debian Edu Wheezy main server.

How to use it:

  # Get a copy of the Squeeze LDAP database
  ssh root@squeeze-tjener "service slapd stop"
  ssh root@squeeze-tjener "slapcat" > tjener-squeeze.ldif
  ssh root@squeeze-tjener "service slapd start"

  # Fetch Kerberos master key used to encrypt user passwords
  ssh root@squeeze-tjener "klist -K -k /etc/krb5kdc/stash"

  # Get a copy of the current Wheezy LDAP database
  service slapd stop
  slapcat > tjener-wheezy.ldif
  service slapd start              # Restart local LDAP server

  ldap-migrate-squeeze-wheezy      # Create input file for ldapmodify to merge

  ldapadmindn=$(ldapsearch -H ldap://localhost/ -x "(&(cn=admin)(objectClass=simpleSecurityObject))" 2>/dev/null | perl -p0e 's/\n //g' | awk '/^dn: / {print $2}')
  ldapmodify -H ldap://localhost/ -ZZ -D "$ldapadmindn" -W -v -x < tjener-wheezy-ldapmodify.ldif # Load the new/changed entries into LDAP

  # List current key with KNVO 1
  klist -K -k /etc/krb5kdc/stash

  # Add old Kerberos master key used to encrypt user passwords as KNVO 2
  kdb5_util add_mkey

  # Add new Kerberos master key to get a KNVO number higher than the old key
  kdb5_util add_mkey

  # Activate key with KNVO 3
  kdb5_util use_mkey 3

  # Migrate all users to KNVO 3
  kdb5_util update_princ_encryption

  # Store key with KNVO 3 in /etc/krb5kdc/stash
  kdb5_util stash

  # Remove now obsolete keys with KNVO 1 and 2
  kdb5_util purge_mkeys -v

  # Copy home directories from old to new main-server
  rsync -av root@squeeze-tjener:/skole/tjener/home0/. /skole/tjener/home0/.

WARNING: This code is experimental!
EOF
    exit($exitcore) if $exitcode;
}

use strict;
use warnings;

use Getopt::Std;
use Net::LDAP::LDIF;
use Data::Dumper;

my $debug = 0;
my %opts;
getopts("d", \%opts) || usage(1);
$debug = 1 if $opts{d};

my $oldldiffile = "tjener-squeeze.ldif";
my $curldiffile = "tjener-wheezy.ldif";
my $newldiffile = "tjener-wheezy-ldapmodify.ldif";

my $oldldif = Net::LDAP::LDIF->new( $oldldiffile, "r", onerror => 'undef' );
unless ($oldldif) { warn "unable to read $oldldiffile"; usage(1); }
my $curldif = Net::LDAP::LDIF->new( $curldiffile, "r", onerror => 'undef' );
unless ($curldif) { warn "unable to read $curldiffile"; usage(1); }
my $newldif = Net::LDAP::LDIF->new( $newldiffile, "w", onerror => 'undef',
                                    change => 1 );
unless ($newldif) { warn "unable to write $newldiffile"; usage(1); }

my %curuser;
my %curgroup;
my %cursudorole;
my %curnisnetgroup;
my %curhost;
while (not $curldif->eof() ) {
    my $entry = $curldif->read_entry();
    if ( ! $curldif->error() ) {
        my %cls;
        map { $cls{$_} = 1 } $entry->get_value('objectClass');
	print Data::Dumper->Dump([\%cls], [qw(*cls)]) if $debug;
        if (exists $cls{'posixAccount'} && exists $cls{'person'}
            && ! exists $cls{'gosaUserTemplate'}
            && ! exists $cls{'gotoWorkstation'}) {
            $curuser{$entry->get_value('uid')} = 1;
        } elsif (exists $cls{'posixGroup'} ) {
            $curgroup{$entry->get_value('cn')} = $entry;
        } elsif (exists $cls{'sudoRole'} ) {
            $cursudorole{$entry->get_value('cn')} = $entry;
        } elsif (exists $cls{'nisNetgroup'} ) {
            $curnisnetgroup{$entry->get_value('cn')} = $entry;
        } elsif (exists $cls{'device'}
                 || exists $cls{'goServer'}
                 || exists $cls{'gotoWorkstation'}) {
            $curhost{$entry->get_value('cn')} = $entry;
        }
    }
}

print Data::Dumper->Dump([\%curuser], [qw(*curuser)]) if $debug;
print Data::Dumper->Dump([\%curgroup], [qw(*curgroup)]) if $debug;
print Data::Dumper->Dump([\%curnisnetgroup], [qw(*curnisnetgroup)]) if $debug;

# Extract every user and group LDAP object not already in the LDAP
# database.
while (not $oldldif->eof() ) {
    my $entry = $oldldif->read_entry();
    if ( $oldldif->error() ) {
        print "Error msg: ", $oldldif->error( ), "\n";
        print "Error lines:\n", $oldldif->error_lines( ), "\n";
    } else {
        my %cls;
        map { $cls{$_} = 1 } $entry->get_value('objectClass');
        if (exists $cls{'posixAccount'} && exists $cls{'person'}
            && ! exists $cls{'gosaUserTemplate'}
            && ! exists $cls{'gotoWorkstation'}) {
            my $uid = $entry->get_value('uid');
            if (!exists ($curuser{$uid})) {
                $entry = trim_internal_attributes_from_entry($entry);
                $newldif->write_entry($entry);
            }
        } elsif (exists $cls{'sudoRole'}) {
            my $cn = $entry->get_value('cn');
            if (!exists ($cursudorole{$cn})) {
                $entry = trim_internal_attributes_from_entry($entry);
                $newldif->write_entry($entry);
            }
        } elsif (exists $cls{'posixGroup'} ) {
	    change_or_add($newldif, $entry, \%curgroup, ['memberUid']);
        } elsif (exists $cls{'nisNetgroup'}) {
	    change_or_add($newldif, $entry, \%curnisnetgroup, ['memberNisNetgroup', 'nisNetgroupTriple']);
        } elsif (exists $cls{'device'}
                 || exists $cls{'goServer'}
                 || exists $cls{'gotoWorkstation'}) {
            my $cn = $entry->get_value('cn');
            if (!exists ($curhost{$cn})) {
                $entry = trim_internal_attributes_from_entry($entry);
                $newldif->write_entry($entry);
            }
        }
	print Data::Dumper->Dump([\$entry], [qw(*entry)]) if $debug;
    }
}

$newldif->done();
$curldif->done();
$oldldif->done();

sub trim_internal_attributes_from_entry {
    my ($entry) = @_;

    # Drop these attributes from all new LDAP objects.  They are not
    # user settable in OpenLDAP.
    my @dropattr = qw(creatorsName entryUUID structuralObjectClass
                      createTimestamp entryCSN modifiersName
                      modifyTimestamp);

    for my $attr (@dropattr) {
        $entry->delete( $attr => []);
    }
    return $entry;
}


sub change_or_add {
    my ($newldif, $entry, $group, $attr_names_aref) = @_;

    my $cn = $entry->get_value('cn');

    if (exists ($group->{$cn})) {
	# check membership of both groups and create change records
	my $curentry = $group->{$cn};

	for my $attr_name (@$attr_names_aref) {
	    my @oldmembers = sort $entry->get_value($attr_name);
	    my @curmembers = sort $curentry->get_value($attr_name);
	    print "Cur: ", Dumper(\@curmembers), "\n" if $debug;
	    print "Old: ", Dumper(\@oldmembers), "\n" if $debug;
	    my %curmemhash;
	    map { $curmemhash{$_} = 1 } @curmembers;
	    my $newentry;
	    my @newmembers;
	    for my $oldmember (@oldmembers) {
		if (!exists $curmemhash{$oldmember}) {
		    print "Adding $oldmember to group $cn\n" if $debug;
		    if (! defined $newentry) {
			$newentry = $entry->clone();
			$newentry->changetype('modify');
		    }
		    push(@newmembers, $oldmember);
		}
	    }
	    if (@newmembers) {
		$newentry->replace($attr_name =>
				   [@curmembers, @newmembers]);
		$newldif->write_entry($newentry);
	    }
	}
    } else {
	# Missing entry, just add it
	$entry = trim_internal_attributes_from_entry($entry);
	$newldif->write_entry($entry);
    }
}
