#!/usr/bin/perl
# 
# Copyright (c) 2007, 2008, 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 generates a LocalKDC and provisions LKDC service principals.
# The script is non-destructive - it can be run multiple times.
#
# Flow
# =======
#   1. Redirect output to /Library/Logs/LKDC-setup
#   2. generate: create the LKDC certificates
#   3. Fetch LKDC name
#     3.1 generate: generate the name from the certificatre
#     3.2 restore: get name from KerberosKDC.plist in DSLocal
#   4. If no m-key, create, if there was none, force KDC generation
#   5. Create database
#     5.1: generate: init database, force keytab generation
#     5.2: restore: pull out the old database entries
#   6. Create keytabs and plist for services
#   7. If creating the database, make launchd not launch us again
#

use strict;
use File::Basename;

my $configured = "/var/db/.configureLocalKDC";

my $progname = basename ($0);

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

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

umask 022;

close STDOUT;
open STDOUT, ">>/Library/Logs/LKDC-setup.log" || die "Failed to open STDOUT";
open STDERR, ">&STDOUT" || die "Failed to open STDERR";

# print when we are running
system("date");

my $kadmin = '/usr/sbin/kadmin';
my $ktutil = '/usr/sbin/ktutil';
my $hod_admin = '/System/Library/PrivateFrameworks/Heimdal.framework/Helpers/hod-admin';

my $system_keychain = '/Library/Keychains/System.keychain';

# certtool requires a system keychain to be present.  Bootstrap one if it is missing.
if (! -f $system_keychain) {
	system '/usr/sbin/systemkeychain', '-C';
}

my $kdc_db_dir = '/var/db/krb5kdc';
my $kdc_log_dir = '/var/log/krb5kdc';
my $kdc_local = '/var/db/dslocal/nodes/Default/config/KerberosKDC.plist';

my $migration_conf = "${kdc_db_dir}/migration.conf";


my $plist_set = 0;
my $make_keytab_flag = 0;
my $update_plists_flag = 0;

my @kdcsetup_args;
my $krb5kdc_plist = '/System/Library/LaunchDaemons/com.apple.Kerberos.kdc.plist';
my $source_version = '10.5'; # if not given, assume Leopard
my $restore_directory;
my $restore = 0;

my $fixup_disallow_service = 0;

my $argc = scalar @ARGV;
my $i;
for ($i = 0; $i < $argc; ++$i) {
	if ($ARGV[$i] eq '--plist') {
		$plist_set = 1;
		$make_keytab_flag = 0;
		$update_plists_flag = 1;
	} elsif ($ARGV[$i] eq '--source') {
		die "Error: --source requires an argument\n" unless ++$i < $argc;
		$restore_directory = $ARGV[$i];
		$restore = 1;
	} elsif ($ARGV[$i] eq '--source-version') {
		die "Error: --source-version requires an argument\n" unless ++$i < $argc;
		$source_version = $ARGV[$i]
	} elsif ($ARGV[$i] eq '--mode') {
		die "Error: --mode requires an argument\n" unless ++$i < $argc;
		# XXX handle migration assistant here
	} else {
		die "Error: unknown argument $ARGV[$i]\n";
	}
}

if (not $restore) {
    my $res;

    printf("creating system keychain entries\n");

    $res = system '/usr/bin/certtool', 'C', 'com.apple.systemdefault', 'u', 'P', 'v';
    if ($res != 0) {
	unlink $configured;
	die "cert tool failed for com.apple.systemdefault";
    }
    $res = system '/usr/bin/certtool', 'C', 'com.apple.kerberos.kdc', 'u', 'P', 'v', 'x=S';
    if ($res != 0) {
	unlink $configured;
	die "cert tool failed for com.apple.kerberos.kdc";
    }
} else {
    printf("doing a restore from ${restore_directory}\n");
}

system '/System/Library/PrivateFrameworks/KerberosHelper.framework/Helpers/lkdc_acl',
     '-s', 'com.apple.kerberos.kdc', '-a',
    '/System/Library/PrivateFrameworks/Heimdal.framework/Helpers/kdc';

my $LKDC_realm;

if (not $restore) {
	my $REALM;

	if (not open REALM, '/System/Library/PrivateFrameworks/KerberosHelper.framework/Helpers/lkdc-cert-hash|' ) {
	    die "failed to get realm of LocalKDC\n";
	}

	$LKDC_realm = <REALM>;
	close REALM;
} else {
	$kdc_local = $restore_directory."/private".$kdc_local;
	my $KDC_LOCAL;
	if (not open (KDC_LOCAL, '/usr/libexec/PlistBuddy -c "Print :realname:0" "' . ${kdc_local} . '" |')) {
		print 'Error: '. $progname ." failed to find $kdc_local\n";
		exit 1;
	}
	$LKDC_realm = <KDC_LOCAL>;
	close KDC_LOCAL;
}

$LKDC_realm =~ s/^.*(LKDC:SHA1\.[0-9A-F]{40}).*$/\1/s;
if ($LKDC_realm eq '') {
	print 'Error: '. $progname ." cannot parse $kdc_local\n";
	exit 1;
}

print "using LKDC realm: ${LKDC_realm}\n";

mkdir(${kdc_db_dir}, 0700) if (not -d ${kdc_db_dir});
mkdir(${kdc_log_dir}, 0700) if (not -d ${kdc_log_dir});

# init the realm

my $force_reinit = 0;
my $res;
my @args = ('-l', '-r', $LKDC_realm) ;

if (not -f "${kdc_db_dir}/m-key") {
    printf("no mkey, creating one\n");
    $res = system(${kadmin}, @args, 'stash',
		  '--random-password', '--no-print-password');
    if ($res ne 0) {
	print "kadmin stash failed\n";
	exit 1;
    }
    $force_reinit = 1;
}

if (not $restore) {

    if (not `dscl /Local/Default read /Users/_krbtgt dsAttrTypeNative:KerberosKeys | grep 'dsAttrTypeNative:KerberosKeys:'`) {
	print "no krbtgt kerberos keys, forcing re-init\n";
	$force_reinit = 1;
    }

    if ($force_reinit) {
	printf("init database\n");
	
	$res = system(${kadmin}, @args,
		      'init', '--bare',
		      '--realm-max-ticket-life=unlimited',
		      '--realm-max-renewable-life=unlimited',
		      'WELLKNOWN:COM.APPLE.LKDC');
	
	if ($res ne 0) {
	    print "kadmin init failed\n";
	    return 1;
	}
	
	if ($plist_set eq 0 or $force_reinit ne 0) {
	    $make_keytab_flag = 1;
	    $update_plists_flag = 1;
	}
	print "$LKDC_realm created\n";
    } else {
	print "$LKDC_realm reused\n";
    }

} elsif ( -f "${restore_directory}/private${kdc_db_dir}/principal.${LKDC_realm}" || $source_version > 10.6) {
    my $CONF;

    my $f = "${kdc_db_dir}/migration.conf";
    my $df = "${kdc_db_dir}/lkdc-dump";

    my @dump_args = ('-c', $migration_conf, '-l', '-r', $LKDC_realm) ;

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

    printf "dumping database\n";
    system $kadmin, @dump_args, "dump", "-d", $df;

    my $OLD_DUMP;
    open OLD_DUMP, $df;
    my @dump = <OLD_DUMP>;
    close OLD_DUMP;

    # XXXX filter out everything except users
    my @princs;
    my $NEW_DUMP;
    open NEW_DUMP, ">".${df}.".new";
    foreach my $d (@dump) {
	if ($d =~ /(^[^\/]*[^\$])\@/) {
	    push(@princs, $1);
	    print NEW_DUMP $d;
	}
    }
    close NEW_DUMP;

    open CONF, ">${migration_conf}";
    print CONF "[kdc]\n";
    print CONF "database = {\n";
    print CONF "\tdbname = od:/Local/Default\n";
    print CONF "\tmkey_file = FILE:${kdc_db_dir}/m-key\n";
    print CONF "\tlog_file = ${kdc_db_dir}/log-${LKDC_realm}\n";
    print CONF "}\n";
    close CONF;

    printf "merge in new database\n";
    system $kadmin, @args, "merge", "--fix-salts", "${df}.new";

    unlink $df;
    unlink "${df}.new";

    foreach my $p (@princs) {
	system $hod_admin, ".", "principal-setflags", $p, "RequireStrongPreAuth";
    }
} elsif (not $plist_set) {
    $make_keytab_flag = 0;
    $update_plists_flag = 0;
}

if ($make_keytab_flag or $update_plists_flag) {
	my %afp_config = (service => 'afpserver', realm => $LKDC_realm,
			  prefs => '/Library/Preferences/com.apple.AppleFileServer',
			  key => 'kerberosPrincipal', format => '%s/%s@%2$s');

	my %smb_config = (service => 'cifs', realm => $LKDC_realm,
			  prefs => '/Library/Preferences/SystemConfiguration/com.apple.smb.server',
       			  key => 'LocalKerberosRealm', format => '%2$s');
 
	my %ss_config  = (service => 'vnc', realm => $LKDC_realm);

	my %host_config  = (service => 'host', realm => $LKDC_realm);

	my $principal = sprintf "%s/%s@%s", "host", $LKDC_realm, $LKDC_realm;

	if ($make_keytab_flag) {
	    print  "$kadmin add $principal\n";
	    system $kadmin, @args, 'add', '--use-defaults', '--random-key', $principal;
	    system $kadmin, @args, 'mod', '-a', '-disallow-svr', $principal;
	}
	    
	configure_service (\%afp_config, $make_keytab_flag, $update_plists_flag);
	configure_service (\%smb_config, $make_keytab_flag, $update_plists_flag);
	configure_service (\%ss_config,  $make_keytab_flag, $update_plists_flag);
	configure_service (\%host_config,  $make_keytab_flag, $update_plists_flag);
}

unlink($migration_conf) if ($restore);

# restore just migrates user, rerun provisioning of krbtgt on next boot
if (not $restore and not $plist_set) {
    system "touch", $configured;
} else {
    unlink $configured;
}

sub configure_service {
	my $config = shift;
	my $do_keytab = shift;
	my $do_plist  = shift;
	my $realm = $config->{realm};

	my $principal = sprintf "%s/%s@%s", $config->{service}, $realm, $realm;

	if ($do_keytab) {

		system $ktutil, 'remove', '-p', $principal;

		print  "$kadmin ext_keytab\n";
		system $kadmin, @args, 'ext_keytab', $principal;
	} 

	if ($do_plist and defined ($config->{prefs})) {
		my $value = sprintf $config->{format}, $config->{service}, $realm;
		my @args = ('write', $config->{prefs}, $config->{key}, $value);

		print  '/usr/bin/defaults '. join (' ', @args). "\n";
		system '/usr/bin/defaults', @args;
	}
}
