#!/usr/bin/perl
# 
# Copyright (c) 2009 Apple Inc. All rights reserved.
#
# @APPLE_LICENSE_HEADER_START@
#
# This file contains Original Code and/or Modifications of Original Code
# as defined in and that are subject to the Apple Public Source License
# Version 2.0 (the 'License'). You may not use this file except in
# compliance with the License. Please obtain a copy of the License at
# http://www.opensource.apple.com/apsl/ and read it before using this
# file.
#
# The Original Code and all software distributed under the License are
# distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
# EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
# INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
# Please see the License for the specific language governing rights and
# limitations under the License.
#
# @APPLE_LICENSE_HEADER_END@
#
# This script migrates a LocalKDC database

use strict;
use Errno qw(:POSIX);
use File::Basename;

my $progname = basename ($0);

chdir '/' or die "chdir: $!\n";

if ($< != 0) {
	print 'Error: '. $progname ." needs to be run by root\n";
	exit EPERM();
}

umask 022;

my ($source);
my $source_version = '10.5'; # if not given, assume Leopard

my $argc = scalar @ARGV;
my $i;
for ($i = 0; $i < $argc; ++$i) {
	if ($ARGV[$i] eq '--source') {
		die "Error: --source requires an argument\n" unless ++$i < $argc;
		$source = $ARGV[$i];
	} elsif ($ARGV[$i] eq '--source-version') {
		die "Error: --source-version requires an argument\n" unless ++$i < $argc;
		$source_version = $ARGV[$i]
	} else {
		die "Error: unknown argument $ARGV[$i]\n";
	}
}

die "--source not defined" unless defined $source;

#
# For pre-Leopard, skip migration
#

$source_version >= 10.5 or exit 0;

#
# If there is no database, skip migration
#

-d "$source/var/db/krb5kdc" or exit 0;

#
# Read in realm names, target and source realm names
#

my $KDC_LOCAL;
my $kdc_local = '/var/db/dslocal/nodes/Default/config/KerberosKDC.plist';

if (not open (KDC_LOCAL, '/usr/libexec/PlistBuddy -c "Print :realname:" '. $kdc_local. '|')) {
	print 'Error: '. $progname ." failed to find $kdc_local\n";
	exit 1;
}
my $targetrealm = join ('', <KDC_LOCAL>);
close KDC_LOCAL;
$targetrealm =~ s/^.*(LKDC:SHA1\.[0-9A-F]{40}).*$/\1/s;

if ($targetrealm eq '') {
	print 'Error: '. $progname ." cannot parse $kdc_local\n";
	exit 1;
}

my $source_kdc_local = $source.$kdc_local;
if (not open (KDC_LOCAL, '/usr/libexec/PlistBuddy -c "Print :realname:" '. $source_kdc_local. '|')) {
	print 'Error: '. $progname ." failed to find $kdc_local\n";
	exit 1;
}
my $configrealm = join ('', <KDC_LOCAL>);
close KDC_LOCAL;
$configrealm =~ s/^.*(LKDC:SHA1\.[0-9A-F]{40}).*$/\1/s;

if ($configrealm eq '') {
	print 'Error: '. $progname ." cannot parse $source_kdc_local\n";
	exit 1;
}

my $migrationkdcconf = "/var/db/krb5kdc/migration.conf";
my $migrationdump = "/var/db/krb5kdc/lkdc-migration-dump";

my $CONF;

open CONF, ">$migrationkdcconf";
print CONF "[kdc]\n";
print CONF "database = {\n";
if ($source_version < 10.7) {
    print CONF "\tdbname = mit-db:$source/var/db/krb5kdc/principal.$configrealm\n";
    print CONF "\tmkey_file = FILE:$source/var/db/krb5kdc/.k5.$configrealm\n";
    print CONF "\tlog_file = /var/db/krb5kdc/log-$configrealm\n";
} else {
    print CONF "\tdbname = od:/Local/Default&$source/var/db/dslocal/nodes/Default\n";
    print CONF "\tmkey_file = $source/var/db/krb5kdc/m-key\n";
    print CONF "\tlog_file = /var/db/krb5kdc/log-$configrealm\n";
}
print CONF "}\n";
close CONF;

die "no Local KDC realm in source kdc.conf" unless defined $configrealm;

print "-----> targetrealm: $targetrealm\n";
print "-----> configrealm: $configrealm\n";

#
# Load in the database and transfor the dumpfile to the new masterkey
#

$ENV{'KRB5_KDC_PROFILE'} = $migrationkdcconf;

open IN, "/usr/sbin/kadmin -c $migrationkdcconf -l -r $configrealm dump -d |";
open OUT, ">$migrationdump";
my (%sourceusers, %targetusers, $user, $realm, $sourcerealm);

#
# Iterate over the data base and strip out all principals that doesn't
# seem to be users and save them in the output dump file
#

while(<IN>) {
    next if (not m/^([^@]*)\@([^ \t]*)\s+/);
    ($user, $realm) = ($1, $2);
    # skip admin/meta principals
    next if ($user eq "K/M");
    next if ($user eq "_krbtgt");
    next if ($user =~ "^kadmin/"); # ignore kadmin foo
    next if ($user =~ m@^krbtgt/.*@); # ignore tgt and cross realm
    next if ($user =~ m@^.*/LKDC:*@); # ignore LKDC services

    print OUT $_;

    $sourceusers{$user} = 1;

    #
    # Verify all principals are from the same realm
    #

    if (defined $sourcerealm and ($sourcerealm ne $realm)) {
	print "Error: $progname realm ($realm) changed ".
	    "source realm ($sourcerealm)\n";
	exit EINVAL();
    }
    $sourcerealm = $realm;

}
close IN;
close OUT;

#
# Switch over to the new database
#

$ENV{'KRB5_KDC_PROFILE'} = '/var/db/krb5kdc/kdc.conf';

#
# check that the dump contained what we expected
#

if ($configrealm ne $sourcerealm) {
    die "missmatch between database and kdc.conf realm" 
}

print "-----> migration kdc.conf: $migrationkdcconf\n";
print "-----> migration dump: $migrationdump\n";

# Load the migration dump into the database
system("/usr/sbin/kadmin -l -r $targetrealm merge --fix-salts /var/db/krb5kdc/lkdc-migration-dump");

# Fix the attributes
foreach my $a (keys %sourceusers) {
    print "-----> fixing attributes: $a\n";
    system("/usr/sbin/kadmin -l -r $targetrealm ".
        "modify -a -disallow-tgt-based $a\@$targetrealm");
    system("/usr/sbin/kadmin -l -r $targetrealm ".
        "modify -a -disallow-forwardable $a\@$targetrealm");
    system "/System/Library/PrivateFrameworks/Heimdal.framework/Helpers/hod-admin . ".
        "principal-setflags $a RequireStrongPreAuth";
}

unlink $migrationkdcconf;
unlink $migrationdump;

#
# Restart KDC since to might have cached part of the database
# and the it will have the wrong data
#

system("killall krb5kdc");

exit 0;
