KLSController.m   [plain text]


/*
 * KLSController.m
 *
 * $Header: /cvs/kfm/KerberosFramework/KerberosLogin/Sources/KerberosLoginServer/KLSController.m,v 1.25 2003/09/12 15:53:09 lxs Exp $
 *
 * Copyright 2003 Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * Export of this software from the United States of America may
 * require a specific license from the United States Government.
 * It is the responsibility of any person or organization contemplating
 * export to obtain such a license before exporting.
 * 
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 */

#import "KLMachIPC.h"
#import "KerberosLoginIPCServer.h"

#import "KLSLifetimeFormatter.h"
#import "KLSController.h"

// Callback function for the Kerberos Framework

krb5_error_code __KLSPrompter (krb5_context  context,
                               void         *data,
                               const char   *name,
                               const char   *banner,
                               int           num_prompts,
                               krb5_prompt   prompts[])
{
    krb5_error_code result = 0;

    KLSController *controller = [NSApp delegate];
    if (controller == NULL) {
        result = klFatalDialogErr;
    } else {
        result = [controller promptWithTitle: name
                                      banner: banner
                                     prompts: prompts
                                 promptCount: num_prompts];
    }
    
    return result;
}

// ---------------------------------------------------------------------------
#pragma mark -

@implementation KLSController

- (id) init
{
    if (self = [super init]) {
        loginState.acquiredPrincipalString = [[NSMutableString alloc] init];
        loginState.acquiredCacheNameString = [[NSMutableString alloc] init];
    }
    return self;
}

- (void) dealloc
{
    [loginState.acquiredPrincipalString  release];
    [loginState.acquiredCacheNameString  release];
    
    [super dealloc];
}

- (void) awakeFromNib
{
    // Fill in the version fields in the dialogs
    NSString *versionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"KLSDisplayVersion"];
    if (versionString == NULL) {
        versionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleGetInfoString"];
    }
    if (versionString != NULL) {
        [loginVersionTextField setStringValue: versionString];
        [changePasswordVersionTextField setStringValue: versionString];
        [prompterVersionTextField setStringValue: versionString];
    } else {
        [loginVersionTextField setStringValue: @""];
        [changePasswordVersionTextField setStringValue: @""];
        [prompterVersionTextField setStringValue: @""];
    }
    
    // remember the dialog frame and height
    singleResponseFrame = [prompterResponsesBox frame];
    prompterWindowSize = [[prompterWindow contentView] frame].size;
    prompterWindowSize.height -= singleResponseFrame.size.height;
    
    // remember the size of the login window frame size with and without the options visible
    loginWindowOptionsOnFrameHeight = loginWindowOptionsOffFrameHeight = [loginWindow frame].size.height;
    loginWindowOptionsOffFrameHeight -= [loginOptionsBox frame].size.height;

    // save these so we can add and remove them
    [loginUsernameTextField retain];
    [loginRealmComboBox retain];
    [loginCallerProvidedUsernameTextField retain];
    [loginCallerProvidedRealmTextField retain];

    // by default, caller did not provide a principal
    [loginCallerProvidedUsernameTextField removeFromSuperview];
    [loginCallerProvidedRealmTextField removeFromSuperview];
}

#pragma mark -

// Notifications

- (void) applicationDidFinishLaunching: (NSNotification *) aNotification
{
    // Initialize the Login Library with our graphical prompter
    __KLSetApplicationPrompter (__KLSPrompter);
    
    // Launch the server after starting the application so that all the 
    // autorelease pools associated with the current run loop have been
    // created and initialized.  Otherwise we leak memory.

    // Initialize the mach server immediately so we start queueing events
    kern_return_t err = mach_server_init (LoginMachIPCServiceName, KerberosLoginIPC_server);
    if (err != KERN_SUCCESS) {
        dprintf ("%s server failed to launch: %s (%d)", LoginMachIPCServiceName, 
                    mach_error_string (err), err);
        exit (1);
    }
    
    err = mach_server_run_server ();
    if (err != KERN_SUCCESS) {
        dprintf ("KerberosLoginServer failed to start up...");
        exit (1);
    }

    exit (0);
}

- (void) controlTextDidChange: (NSNotification *) aNotification
{
    // Not sure which dialog it is, so just update both:
	[self loginUpdateOkButton];
	[self changePasswordUpdateOkButton];
}

- (void) comboBoxSelectionDidChange: (NSNotification *) aNotification
{
    [self loginUpdateOkButton];
}

- (void) comboBoxWillDismiss: (NSNotification *) aNotification
{
    [self loginUpdateOkButton];
}


#pragma mark -

// Login Window

- (KLStatus) getTicketsForPrincipal: (const char *) principalUTF8String
                              flags: (krb5_flags) flags
                           lifetime: (KLLifetime) lifetime
                  renewableLifetime: (KLLifetime) renewableLifetime
                          startTime: (KLTime) startTime
                        forwardable: (KLBoolean) forwardable
                          proxiable: (KLBoolean) proxiable
                        addressless: (KLBoolean) addressless
                        serviceName: (const char *) serviceName
                    applicationName: (const char *) applicationNameString
                applicationIconPath: (const char *) applicationIconPathString
{
    KLStatus err = klNoErr;
    KLSize typeSize;
    KLLifetime maxLifetime, minLifetime;
    KLBoolean renewable = (flags & KRB5_GET_INIT_CREDS_OPT_RENEW_LIFE);
    KLBoolean showOptions = false;

    if (applicationNameString != NULL) {
        [loginHeaderTextField setStringValue: [NSString stringWithFormat: NSLocalizedString (@"KLApplicationRequest", NULL),
            [NSString stringWithUTF8String: applicationNameString]]];
    } else {
        [loginHeaderTextField setStringValue: NSLocalizedString (@"KLUnknownRequest", NULL)];
    }
    
    // Set up badge icon:
    if (applicationIconPathString != NULL) {
        NSImage *badgeIconImage = [[NSWorkspace sharedWorkspace] iconForFile:
            [NSString stringWithCString: applicationIconPathString]];
        [loginKerberosIconImageView setBadgeIconImage: badgeIconImage];
    } else {
        [loginKerberosIconImageView setBadgeIconImage: NULL];
    }
    
    ////////////////////////////////////
    // Setup Name, Realm and Password //
    ////////////////////////////////////
    
    if (principalUTF8String != NULL) {
        [self loginUpdateWithCallerProvidedPrincipal: YES];
        loginState.usernameControl = loginCallerProvidedUsernameTextField;
        loginState.realmControl = loginCallerProvidedRealmTextField;
        
#pragma warning Need support for "\@" in realm names
        NSString *principalString = [NSString stringWithUTF8String: principalUTF8String];
        NSRange separator = [principalString rangeOfString: @"@" options: (NSLiteralSearch | NSBackwardsSearch)];

        [loginState.usernameControl setStringValue: [principalString substringToIndex: separator.location]];
        [loginState.realmControl setStringValue: [principalString substringFromIndex: separator.location + separator.length]];
    } else {
        KLIndex defaultRealmIndex;
        KLIndex realmCount = KLCountKerberosRealms ();

        [self loginUpdateWithCallerProvidedPrincipal: NO];
        loginState.usernameControl = loginUsernameTextField;
        loginState.realmControl = loginRealmComboBox;

        if (KLGetDefaultLoginOption (loginOption_LoginName, NULL, &typeSize) == klNoErr) {
            char *name = (char *) malloc (sizeof (char) * (typeSize + 1));

            if (name != NULL) {
                if (KLGetDefaultLoginOption (loginOption_LoginName, name, &typeSize) == klNoErr) {
                    name [typeSize] = '\0'; // NUL-terminate so we can look it up as a UTF8String
                    [loginState.usernameControl setStringValue: [NSString stringWithUTF8String: name]];
                }

                free (name);
            }
        }

        // Fill in the rest of the realms into the combo box
        if (realmCount > 0) {
            int i;

            for (i = 0; i < realmCount; i++) {
                char *realm;
                if (KLGetKerberosRealm (i, &realm) == klNoErr) {
                    [loginRealmComboBox addItemWithObjectValue: [NSString stringWithUTF8String: realm]];
                    KLDisposeString (realm);
                }
            }
        }
        
        if (KLGetKerberosDefaultRealm (&defaultRealmIndex) == klNoErr) {
            [loginRealmComboBox selectItemAtIndex: defaultRealmIndex];
        } else if ([loginRealmComboBox numberOfItems] > 0) {
            [loginRealmComboBox selectItemAtIndex: 0];
        }
        [loginRealmComboBox setObjectValue: [loginRealmComboBox numberOfItems] > 0 ? [loginRealmComboBox objectValueOfSelectedItem] : @""];
        [loginRealmComboBox setNumberOfVisibleItems: [loginRealmComboBox numberOfItems]];
        [loginRealmComboBox setCompletes: YES];
    }

    // Clear the password text field
    [loginPasswordSecureTextField setObjectValue: @""];

    // Set the text insertion pointer location
    if ([[loginState.usernameControl stringValue] length] > 0) {
        [loginWindow setInitialFirstResponder: loginPasswordSecureTextField];
    } else {
        [loginWindow setInitialFirstResponder: loginState.usernameControl];
    }

    //////////////////////////
    // Setup Ticket Options //
    //////////////////////////

    // Fill in options with KLL defaults if not already overridden by client
    if (!(flags & KRB5_GET_INIT_CREDS_OPT_FORWARDABLE)) {
        typeSize = sizeof (forwardable);
        err = KLGetDefaultLoginOption (loginOption_DefaultForwardableTicket, &forwardable, &typeSize);
        if (err != klNoErr) {
            dprintf ("KLGetDefaultLoginOption (loginOption_DefaultForwardableTicket) returned %s (%ld)\n", error_message (err), err);
        } else {
            flags |= KRB5_GET_INIT_CREDS_OPT_FORWARDABLE;
        }
    }

    if (!(flags & KRB5_GET_INIT_CREDS_OPT_PROXIABLE)) {
        typeSize = sizeof (proxiable);
        err = KLGetDefaultLoginOption (loginOption_DefaultProxiableTicket, &proxiable, &typeSize);
        if (err != klNoErr) {
            dprintf ("KLGetDefaultLoginOption (loginOption_DefaultProxiableTicket) returned %s (%ld)\n", error_message (err), err);
        } else {
            flags |= KRB5_GET_INIT_CREDS_OPT_PROXIABLE;
        }
    }

    if (!(flags & KRB5_GET_INIT_CREDS_OPT_ADDRESS_LIST)) {
        typeSize = sizeof (addressless);
        err = KLGetDefaultLoginOption (loginOption_DefaultAddresslessTicket, &addressless, &typeSize);
        if (err != klNoErr) {
            dprintf ("KLGetDefaultLoginOption (loginOption_DefaultAddresslessTicket) returned %s (%ld)\n", error_message (err), err);
        } else {
            flags |= KRB5_GET_INIT_CREDS_OPT_ADDRESS_LIST;
        }
    }

    if (!(flags & KRB5_GET_INIT_CREDS_OPT_RENEW_LIFE)) {
        typeSize = sizeof (renewable);
        err = KLGetDefaultLoginOption (loginOption_DefaultRenewableTicket, &renewable, &typeSize);
        if (err != klNoErr) {
            dprintf ("KLGetDefaultLoginOption (loginOption_DefaultRenewableTicket) returned %s (%ld)\n", error_message (err), err);
        } else {
            if (renewable) { flags |= KRB5_GET_INIT_CREDS_OPT_RENEW_LIFE; }
        }
        typeSize = sizeof (renewableLifetime);
        err = KLGetDefaultLoginOption (loginOption_DefaultRenewableLifetime, &renewableLifetime, &typeSize);
        if (err != klNoErr) {
            dprintf ("KLGetDefaultLoginOption (loginOption_DefaultRenewableLifetime) returned %s (%ld)\n", error_message (err), err);
        }
    }

    if (!(flags & KRB5_GET_INIT_CREDS_OPT_TKT_LIFE)) {
        typeSize = sizeof (lifetime);
        err = KLGetDefaultLoginOption (loginOption_DefaultTicketLifetime, &lifetime, &typeSize);
        if (err != klNoErr) {
            dprintf ("KLGetDefaultLoginOption (loginOption_DefaultTicketLifetime) returned %s (%ld)\n", error_message (err), err);
        } else {
            flags |= KRB5_GET_INIT_CREDS_OPT_TKT_LIFE;
        }
    }

    // Load the options in the state variables
    loginState.flags = flags;
    loginState.lifetime = lifetime;
    loginState.renewable = renewable;
    loginState.renewableLifetime = renewableLifetime;
    loginState.startTime = startTime;
    loginState.forwardable = forwardable;
    loginState.proxiable = proxiable;
    loginState.addressless = addressless;
    loginState.serviceName = serviceName;

    /////////////////////////////////////
    // Copy Ticket Options to Controls //
    /////////////////////////////////////

    [loginForwardableCheckbox setIntValue: loginState.forwardable];
    [loginAddresslessCheckbox setIntValue: loginState.addressless];
    [loginRenewableCheckbox setIntValue: loginState.renewable];

    // Set up the slider ranges
    typeSize = sizeof (KLLifetime);
    err = KLGetDefaultLoginOption (loginOption_MinimalTicketLifetime, &minLifetime, &typeSize);
    if (err != klNoErr) {
        dprintf ("KLGetDefaultLoginOption (loginOption_MinimalTicketLifetime) returned %s (%ld)\n", error_message (err), err);
        minLifetime = loginState.lifetime;
    }

    err = KLGetDefaultLoginOption (loginOption_MaximalTicketLifetime, &maxLifetime, &typeSize);
    if (err != klNoErr) {
        dprintf ("KLGetDefaultLoginOption (loginOption_MaximalTicketLifetime) returned %s (%ld)\n", error_message (err), err);
        maxLifetime = loginState.lifetime;
    }

    [self loginSetupSlider: loginLifetimeSlider
                 textField: loginLifetimeText
                   minimum: minLifetime
                   maximum: maxLifetime
                     value: loginState.lifetime];

    err = KLGetDefaultLoginOption (loginOption_MinimalRenewableLifetime, &minLifetime, &typeSize);
    if (err != klNoErr) {
        dprintf ("KLGetDefaultLoginOption (loginOption_MinimalRenewableLifetime) returned %s (%ld)\n", error_message (err), err);
        minLifetime = loginState.renewableLifetime;
    }

    err = KLGetDefaultLoginOption (loginOption_MaximalRenewableLifetime, &maxLifetime, &typeSize);
    if (err != klNoErr) {
        dprintf ("KLGetDefaultLoginOption (loginOption_MaximalRenewableLifetime) returned %s (%ld)\n", error_message (err), err);
        maxLifetime = loginState.renewableLifetime;
    }

    [self loginSetupSlider: loginRenewableLifetimeSlider
                 textField: loginRenewableLifetimeText
                   minimum: minLifetime
                   maximum: maxLifetime
                     value: loginState.renewableLifetime];

    [self loginRenewableCheckboxWasHit: self]; // update the enabled/disabledness off the slider

    // Update the state of the options:
    typeSize = sizeof (KLBoolean);
    err = KLGetDefaultLoginOption (loginOption_ShowOptions, &showOptions, &typeSize);
    if (err != klNoErr) { showOptions = false; }
    [loginOptionsButton setState: showOptions ? NSOnState : NSOffState];
    [self loginOptionsButtonWasHit: self];

    // Run the window until the user hits ok or cancel
    return [self displayAndRunWindow: loginWindow];
}

- (void) loginUpdateOkButton
{
    if (([[loginState.usernameControl   stringValue] length] > 0) &&
        ([[loginState.realmControl      stringValue] length] > 0) &&
        ([[loginPasswordSecureTextField stringValue] length] > 0)) {
        [loginOkButton setEnabled: TRUE];
    } else {
        [loginOkButton setEnabled: FALSE];
    }
}

- (void) loginUpdateWithCallerProvidedPrincipal: (KLBoolean) callerProvidedPrincipal
{
    static KLBoolean callerProvidedPrincipalState = false;

    if (callerProvidedPrincipalState != callerProvidedPrincipal) {
        if (callerProvidedPrincipal) {
            [[loginUsernameTextField superview] addSubview: loginCallerProvidedUsernameTextField];
            [[loginRealmComboBox superview] addSubview: loginCallerProvidedRealmTextField];

            [loginUsernameTextField removeFromSuperview];
            [loginRealmComboBox removeFromSuperview];
        } else {
            [[loginCallerProvidedUsernameTextField superview] addSubview: loginUsernameTextField];
            [[loginCallerProvidedRealmTextField superview] addSubview: loginRealmComboBox];

            [loginCallerProvidedUsernameTextField removeFromSuperview];
            [loginCallerProvidedRealmTextField removeFromSuperview];
        }
        callerProvidedPrincipalState = callerProvidedPrincipal;
    }
}

- (void) loginSetupSlider: (NSSlider *) slider
                textField: (NSTextField *) textField
                  minimum: (int) minimum
                  maximum: (int) maximum
                    value: (int) value
{
    int min = minimum;
    int max = maximum;
    int increment = 0;

    if (max < min) {
        // swap values
        int temp = max;
        max = min;
        min = temp;
    }

    int	range = max - min;

    if (range < 5*60)              { increment = 1;       // 1 second if under 5 minutes
    } else if (range < 30*60)      { increment = 5;       // 5 seconds if under 30 minutes
    } else if (range < 60*60)      { increment = 15;      // 15 seconds if under 1 hour
    } else if (range < 2*60*60)    { increment = 30;      // 30 seconds if under 2 hours
    } else if (range < 5*60*60)    { increment = 60;      // 1 minute if under 5 hours
    } else if (range < 50*60*60)   { increment = 5*60;    // 5 minutes if under 50 hours
    } else if (range < 200*60*60)  { increment = 15*60;   // 15 minutes if under 200 hours
    } else if (range < 500*60*60)  { increment = 30*60;   // 30 minutes if under 500 hours
    } else                         { increment = 60*60; } // 1 hour otherwise

    int roundedMinimum = (min / increment) * increment;
    if (roundedMinimum > min) { roundedMinimum -= increment; }
    if (roundedMinimum <= 0)  { roundedMinimum += increment; }  // ensure it is positive

    int roundedMaximum = (max / increment) * increment;
    if (roundedMaximum < max) { roundedMaximum += increment; }

    int roundedValue = (value / increment) * increment;
    if (roundedValue < roundedMinimum) { roundedValue = roundedMinimum; }
    if (roundedValue > roundedMaximum) { roundedValue = roundedMaximum; }

    if (roundedMinimum == roundedMaximum) {
        [textField setTextColor: [NSColor grayColor]];
        [slider setEnabled: FALSE];
    } else {
        [textField setTextColor: [NSColor blackColor]];
        [slider setEnabled: TRUE];
    }

    // Attach the formatter to the slider
    NSDateFormatter *lifetimeFormatter = [[KLSLifetimeFormatter alloc] initWithMinimum: roundedMinimum
                                                                               maximum: roundedMaximum
                                                                             increment: increment];

    [textField setFormatter: lifetimeFormatter];
    [lifetimeFormatter release];  // the textField will retain it

    [slider setMinValue: 0];
    [slider setMaxValue: (roundedMaximum - roundedMinimum) / increment];
    [slider setIntValue: (roundedValue - roundedMinimum) / increment];
    [textField takeObjectValueFrom: slider];
}

- (IBAction) loginAddresslessCheckboxWasHit: (id) sender
{
    loginState.addressless = [loginAddresslessCheckbox intValue];
}

- (IBAction) loginForwardableCheckboxWasHit: (id) sender
{
    loginState.forwardable = [loginForwardableCheckbox intValue];
}

- (IBAction) loginRenewableCheckboxWasHit: (id) sender
{
    loginState.renewable = [loginRenewableCheckbox intValue];    
}

- (IBAction) loginLifetimeSliderChanged: (id) sender
{
    [loginLifetimeText takeObjectValueFrom: loginLifetimeSlider];
    loginState.lifetime = [[loginLifetimeText formatter] lifetimeForInt: [loginLifetimeSlider intValue]];
}

- (IBAction) loginRenewableLifetimeSliderChanged: (id) sender
{
    [loginRenewableLifetimeText takeObjectValueFrom: loginRenewableLifetimeSlider];
    loginState.renewableLifetime = [[loginRenewableLifetimeText formatter] lifetimeForInt: [loginRenewableLifetimeSlider intValue]];
}

- (IBAction) loginOptionsButtonWasHit: (id) sender
{
    NSRect loginWindowFrameRect = [loginWindow frame];
    
    if ([loginOptionsButton state] == NSOnState) {
        if (loginWindowFrameRect.size.height != loginWindowOptionsOnFrameHeight) {
            loginWindowFrameRect.origin.y -= (loginWindowOptionsOnFrameHeight - loginWindowOptionsOffFrameHeight);
            loginWindowFrameRect.size.height = loginWindowOptionsOnFrameHeight;
            [loginOptionsButton setTitle: NSLocalizedString (@"KLHideOptions", NULL)];
        }

    } else if ([loginOptionsButton state] == NSOffState) {
        if (loginWindowFrameRect.size.height != loginWindowOptionsOffFrameHeight) {
            loginWindowFrameRect.origin.y += (loginWindowOptionsOnFrameHeight - loginWindowOptionsOffFrameHeight);
            loginWindowFrameRect.size.height = loginWindowOptionsOffFrameHeight;
            [loginOptionsButton setTitle: NSLocalizedString (@"KLShowOptions", NULL)];
        }

    } else {
        NSLog (@"loginOptionsButtonWasHit: Unknown state!");
    }
    [loginWindow setFrame: loginWindowFrameRect display: YES animate: YES];
}

- (IBAction) loginCancelButtonWasHit: (id) sender
{
    [NSApp endSheet: loginWindow returnCode: klUserCanceledErr];
}

- (IBAction) loginOkButtonWasHit: (id) sender
{
    KLStatus err = klNoErr;
    NSString *principalString;
    KLLoginOptions loginOptions = NULL;
    KLPrincipal principal = NULL;
    char *cacheName = NULL;
    
    // Get Tickets:
    principalString = [NSString stringWithFormat: @"%@@%@", [loginState.usernameControl stringValue], [loginState.realmControl stringValue]];
    
    if (err == klNoErr) {
        dprintf ("Creating principal for '%s'\n", [principalString UTF8String]);
        err = KLCreatePrincipalFromString ([principalString UTF8String], kerberosVersion_V5, &principal);
    }
    
    if (err == klNoErr) {
        err = KLCreateLoginOptions (&loginOptions);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetTicketLifetime (loginOptions, loginState.lifetime);
    }
    
    if (err == klNoErr) {
        if (loginState.renewable) {
            err = KLLoginOptionsSetRenewableLifetime (loginOptions, loginState.renewableLifetime);
        } else {
            err = KLLoginOptionsSetRenewableLifetime (loginOptions, 0L);
        }
    }

    if (err == klNoErr) {
        err = KLLoginOptionsSetTicketStartTime (loginOptions, loginState.startTime);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetServiceName (loginOptions, loginState.serviceName);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetForwardable (loginOptions, loginState.forwardable);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetProxiable (loginOptions, loginState.proxiable);
    }
    
    if (err == klNoErr) {
        err = KLLoginOptionsSetAddressless (loginOptions, loginState.addressless);
    }
    
    if (err == klNoErr) {
        err = KLAcquireNewInitialTicketsWithPassword (principal, 
                                                      loginOptions, 
                                                      [[loginPasswordSecureTextField stringValue] UTF8String], 
                                                      &cacheName);
        if (err == KRB5KDC_ERR_KEY_EXP) {
            // Ask the user if s/he wants to change the password
            if ([self askYesNoQuestion: NSLocalizedString (@"KLStringPasswordExpired", NULL)] == YES) {
                if ([self changePasswordForPrincipal: [principalString UTF8String]] == klNoErr) {
                    // Okay, we changed the password, now try again with the new password:
                    err = KLAcquireNewInitialTicketsWithPassword (principal, 
                                                                  loginOptions, 
                                                                  [[changePasswordNewPasswordSecureTextField stringValue] UTF8String], 
                                                                  &cacheName);
                }
            }
        }
    }

    if (err == klNoErr) {
        // Save the current settings of the dialog if needed
        [self loginSaveOptionsIfNeeded];

        // Save the principal and cache name
        [loginState.acquiredPrincipalString setString: principalString];
        [loginState.acquiredCacheNameString setString: [NSString stringWithUTF8String: cacheName]];

        [NSApp endSheet: loginWindow returnCode: klNoErr];
    } else if (err != klUserCanceledErr) {
        [self displayKLError: err];
    }

    if (loginOptions != NULL) {
        KLDisposeLoginOptions (loginOptions);
    }
    if (principal != NULL) {
        KLDisposePrincipal (principal);
    }
    if (cacheName != NULL) {
        KLDisposeString (cacheName);
    }
}

- (void) loginSaveOptionsIfNeeded
{
    KLStatus err = klNoErr;
    KLBoolean rememberShowOptions;
    KLBoolean rememberPrincipal;
    KLBoolean rememberExtras;
    KLSize typeSize = sizeof (KLBoolean);

    // Should we remember the principal in the user's preferences?
    err = KLGetDefaultLoginOption (loginOption_RememberPrincipal, &rememberPrincipal, &typeSize);
    if (err == klNoErr && rememberPrincipal) {
        NSString *loginName = [loginState.usernameControl stringValue];
        NSString *loginRealm = [loginState.realmControl stringValue];
        KLIndex loginRealmIndex;

        err = KLSetDefaultLoginOption (loginOption_LoginName, [loginName UTF8String], [loginName length]);
        if (err != klNoErr) dprintf ("KLSetDefaultLoginOption (loginOption_LoginName) returned %s (%ld)\n", error_message (err), err);

        err = KLSetDefaultLoginOption (loginOption_LoginInstance, "", 0);
        if (err != klNoErr) dprintf ("KLSetDefaultLoginOption (loginOption_LoginInstance) returned %s (%ld)\n", error_message (err), err);

        err = KLFindKerberosRealmByName ([loginRealm UTF8String], &loginRealmIndex);
        if (err != klNoErr) {
            err = KLInsertKerberosRealm (realmList_End, [loginRealm UTF8String]); // Add the realm
        }
        if (err == klNoErr) {
            // Either we found the realm or we succeeded in adding it
            err = KLSetKerberosDefaultRealmByName ([loginRealm UTF8String]);
            if (err != klNoErr) dprintf ("KLSetKerberosDefaultRealmByName returned %s (%ld)\n", error_message (err), err);
        }
    }

    // Should we remember the state of the options button?
    err = KLGetDefaultLoginOption (loginOption_RememberShowOptions, &rememberShowOptions, &typeSize);
    if (err == klNoErr && rememberShowOptions) {
        KLBoolean showOptions = ([loginOptionsButton state] == NSOnState);

        err = KLSetDefaultLoginOption (loginOption_ShowOptions, &showOptions, sizeof (showOptions));
        if (err != klNoErr) dprintf ("KLSetDefaultLoginOption (loginOption_ShowOptions) returned %s (%ld)\n", error_message (err), err);
    }

    // Should we remember the ticket options in the user's preferences?
    err = KLGetDefaultLoginOption (loginOption_RememberExtras, &rememberExtras, &typeSize);
    if ((err == klNoErr) && rememberExtras) {
        err = KLSetDefaultLoginOption (loginOption_DefaultForwardableTicket, &loginState.forwardable, sizeof (loginState.forwardable));
        if (err != klNoErr) dprintf ("KLSetDefaultLoginOption (loginOption_DefaultForwardableTicket) returned %s (%ld)\n", error_message (err), err);

        err = KLSetDefaultLoginOption (loginOption_DefaultAddresslessTicket, &loginState.addressless, sizeof (loginState.addressless));
        if (err != klNoErr) dprintf ("KLSetDefaultLoginOption (loginOption_DefaultAddresslessTicket) returned %s (%ld)\n", error_message (err), err);

        err = KLSetDefaultLoginOption (loginOption_DefaultTicketLifetime, &loginState.lifetime, sizeof (loginState.lifetime));
        if (err != klNoErr) dprintf ("KLSetDefaultLoginOption (loginOption_DefaultTicketLifetime) returned %s (%ld)\n", error_message (err), err);

        err = KLSetDefaultLoginOption (loginOption_DefaultRenewableTicket, &loginState.renewable, sizeof (loginState.renewable));
        if (err != klNoErr) dprintf ("KLSetDefaultLoginOption (loginOption_DefaultRenewableTicket) returned %s (%ld)\n", error_message (err), err);

        if (loginState.renewable) {
            err = KLSetDefaultLoginOption (loginOption_DefaultRenewableLifetime, &loginState.renewableLifetime, sizeof (loginState.renewableLifetime));
            if (err != klNoErr) dprintf ("KLSetDefaultLoginOption (loginOption_DefaultRenewableLifetime) returned %s (%ld)\n", error_message (err), err);
        }
    }
}

- (const char *) loginAcquiredPrincipal
{
    return [loginState.acquiredPrincipalString UTF8String];
}

- (const char *) loginAcquiredCacheName
{
    return [loginState.acquiredCacheNameString UTF8String];
}

#pragma mark -

// Change Password Window

- (KLStatus) changePasswordForPrincipal: (const char *) principalUTF8String
{
    [changePasswordPrincipalTextField setObjectValue: [NSString stringWithUTF8String: principalUTF8String]];
    [changePasswordOldPasswordSecureTextField setObjectValue: @""];
    [changePasswordNewPasswordSecureTextField setObjectValue: @""];
    [changePasswordVerifyPasswordSecureTextField setObjectValue: @""];

    // Run until the user hits ok or cancel
    return [self displayAndRunWindow: changePasswordWindow];
}


- (void) changePasswordUpdateOkButton
{
    if ([[changePasswordOldPasswordSecureTextField stringValue] length] > 0
        && [[changePasswordNewPasswordSecureTextField stringValue] length] > 0
        && [[changePasswordVerifyPasswordSecureTextField stringValue] length] > 0) {
        [changePasswordOkButton setEnabled:TRUE];
    } else {
        [changePasswordOkButton setEnabled:FALSE];
    }    
}

- (IBAction) changePasswordCancelButtonWasHit: (id) sender
{
    [NSApp endSheet: changePasswordWindow returnCode: klUserCanceledErr];
}

- (IBAction) changePasswordOkButtonWasHit: (id) sender
{
    KLStatus err = klNoErr;
    KLPrincipal principal = NULL;
    char *rejectionErrorUTF8String = NULL;
    char *rejectionDescriptionUTF8String = NULL;
    Boolean rejected = FALSE;
    
    if ([[changePasswordNewPasswordSecureTextField stringValue] 
         isEqual: [changePasswordVerifyPasswordSecureTextField stringValue]] == false) {
        err = klPasswordMismatchErr;
    }
    
    if (err == klNoErr) {
        err = KLCreatePrincipalFromString ([[changePasswordPrincipalTextField stringValue] UTF8String], kerberosVersion_V5, &principal);
    }
    
    if (err == klNoErr) {
        err = KLChangePasswordWithPasswords (principal, 
                                             [[changePasswordOldPasswordSecureTextField stringValue] UTF8String], 
                                             [[changePasswordNewPasswordSecureTextField stringValue] UTF8String],
                                             &rejected, &rejectionErrorUTF8String, &rejectionDescriptionUTF8String);
    }
    
    if (err == klNoErr) {
        if (rejected) {
           [self displayServerError: rejectionErrorUTF8String description: rejectionDescriptionUTF8String];
        } else {
            [NSApp endSheet: changePasswordWindow returnCode: klNoErr];
        }
    } else if (err != klUserCanceledErr) {
        [self displayKLError: err];
    }
    
    if (principal != NULL) {
        KLDisposePrincipal (principal);
    }
    if (rejectionErrorUTF8String != NULL) {
        KLDisposeString (rejectionErrorUTF8String);
    }
    if (rejectionDescriptionUTF8String != NULL) {
        KLDisposeString (rejectionDescriptionUTF8String);
    }
}

#pragma mark -

- (krb5_error_code) promptWithTitle: (const char *) title
                          banner: (const char *) banner
                          prompts: (krb5_prompt *) prompts
                          promptCount: (int) promptCount
{
    KLStatus err = klNoErr;
    int i;
    NSRect frameRect;
    NSSize windowSize;
    NSMutableArray *responsesArray = [NSMutableArray arrayWithCapacity: promptCount];
    NSString *titleString = (title != NULL) ? [NSString stringWithUTF8String: title] : @"";
    NSString *bannerString = (banner != NULL) ? [NSString stringWithUTF8String: banner] :
                                                NSLocalizedString(@"KLStringPrompterChangeNotice", NULL);

    [prompterWindow setTitle: titleString];
    [prompterBannerTextField setStringValue: bannerString];

    [prompterPromptsMatrix renewRows: promptCount columns: 1];
    [prompterPromptsMatrix sizeToCells];
    
    // Calculate resize the window to hold the new cells.
    // The matrix cells will automatically be moved down.
    windowSize = prompterWindowSize;
    frameRect = [prompterWindow frame];
    windowSize.height += (singleResponseFrame.size.height * promptCount) + (kPrompterResponseSpacing * (promptCount - 1));
    [prompterWindow setContentSize: windowSize];

    for (i = 0; i < promptCount; i++) {
        NSTextField *responseTextField = NULL;

        [[prompterPromptsMatrix cellAtRow: i column: 0] setObjectValue: [NSString stringWithUTF8String: prompts[i].prompt]];

        frameRect = singleResponseFrame;
        frameRect.origin.y += ((promptCount - 1) - i) * (kPrompterResponseSpacing + frameRect.size.height);

        if (prompts[i].hidden) {
            NSSecureTextField *responseSecureTextField = [[NSSecureTextField alloc] initWithFrame: frameRect];

            [[responseSecureTextField cell] setEchosBullets: YES];
            responseTextField = responseSecureTextField;
        } else {
            responseTextField = [[NSTextField alloc] initWithFrame: frameRect];
        }

        [responseTextField setEnabled: YES];
        [responseTextField setEditable: YES];
        [responseTextField setBezeled: YES];
        [responseTextField setBezelStyle: NSTextFieldSquareBezel];
        [responseTextField setAlignment: NSLeftTextAlignment];
        [responseTextField setDrawsBackground: YES];
        [responseTextField setBackgroundColor: [NSColor whiteColor]];
        [responseTextField setFont: [NSFont systemFontOfSize: 13]];
        [responseTextField setStringValue: @""];
        [responseTextField autorelease];
        
        [responsesArray addObject: responseTextField];
        [[prompterWindow contentView] addSubview: responseTextField];
        if (i == 0) {
            [prompterWindow setInitialFirstResponder: responseTextField];
        } else {
            // chain up the text field so we can tab between them
            [[responsesArray objectAtIndex: i - 1] setNextKeyView: responseTextField];
        }
    }

    // connect the last text field to the first one
    if (promptCount > 1) {
        [[responsesArray objectAtIndex: promptCount - 1] setNextKeyView: [responsesArray objectAtIndex: 0]];
    }

    // Run until the user hits ok or cancel
    err = [self displayAndRunWindow: prompterWindow];
    if (err == klNoErr) {
        for (i = 0; i < promptCount; i++) {
            NSTextField *responseTextField = [responsesArray objectAtIndex: i];

            NSString *response = response = [responseTextField stringValue];
            int length = [response length] + 1;
            
            // Make sure it won't overflow:
            if (length > prompts[i].reply->length) 
            	length = prompts[i].reply->length;
                
            memmove (prompts[i].reply->data, [response UTF8String], length * sizeof (char));
            prompts[i].reply->data[length - 1] = '\0';
            prompts[i].reply->length = length;

            [responseTextField removeFromSuperview]; // Remove from prompter window
        }
    }
    
    return err;
}

- (IBAction) prompterCancelButtonWasHit: (id) sender
{
	[NSApp endSheet: prompterWindow returnCode: klUserCanceledErr];
}

- (IBAction) prompterOkButtonWasHit: (id) sender
{
 	[NSApp endSheet: prompterWindow returnCode: klNoErr];
}

#pragma mark -

- (void) displayKLError: (KLStatus) error
{
    NSWindow *parentWindow = [self frontWindow];
    KLStatus err = klNoErr;
    char *descriptionUTF8String;
    
    err = KLGetErrorString (error, &descriptionUTF8String);
    if (err == klNoErr) {
        KLDialogIdentifier identifier = loginLibrary_UnknownDialog;
        
        if (parentWindow == loginWindow) {
            identifier = loginLibrary_LoginDialog;
        } else if (parentWindow == changePasswordWindow) {
            identifier = loginLibrary_ChangePasswordDialog;
        } else if (parentWindow == prompterWindow) {
            identifier = loginLibrary_PrompterDialog;
        }
        
        [self displayKLError: error windowIdentifier: identifier];
    }
}

- (void) displayKLError: (KLStatus) error
                windowIdentifier: (KLDialogIdentifier) identifier
{
    KLStatus err = klNoErr;
    char *descriptionUTF8String;

    // Use a special error when the caps lock key is down and the password was incorrect
    // if the caps lock key is down accidentally, it will still be down now
    switch (error) {
        case INTK_BADPW:
        case KRB5KRB_AP_ERR_BAD_INTEGRITY:
        case KRBET_INTK_BADPW:
        case KRB5KDC_ERR_PREAUTH_FAILED:
        case klBadPasswordErr: {
            unsigned int modifiers = [[NSApp currentEvent] modifierFlags];
            if (modifiers & NSAlphaShiftKeyMask) {
                error = klCapsLockErr;
            }
        }
    }

    err = KLGetErrorString (error, &descriptionUTF8String);
    if (err == klNoErr) {
        NSString *key = @"KLStringUnknownError";
        
        // Get the header string:
        if (identifier == loginLibrary_LoginDialog) {
            key = @"KLStringLoginFailed";
        } else if (identifier == loginLibrary_ChangePasswordDialog) {
            key = @"KLStringChangePasswordFailed";
        } else if (identifier == loginLibrary_PrompterDialog) {
            key = @"KLStringPrompterFailed";
        }
        
        [self displayError: NSLocalizedString (key, NULL)
                description: [NSString stringWithUTF8String: descriptionUTF8String]];
    }
}


- (void) displayServerError: (const char *) errorUTF8String 
                description: (const char *) descriptionUTF8String
{
    // Remove all newlines from the server responses (they look goofy in the dialog):
    NSMutableString *errorString = [NSMutableString stringWithUTF8String: errorUTF8String];
    [errorString replaceOccurrencesOfString: @"\n" withString: @"" 
                        options: NSLiteralSearch 
                        range: NSMakeRange (0, [errorString length])];

    NSMutableString *descriptionString = [NSMutableString stringWithUTF8String: descriptionUTF8String];
    [descriptionString replaceOccurrencesOfString: @"\n" withString: @"" 
                              options: NSLiteralSearch 
                              range: NSMakeRange (0, [descriptionString length])];

    [self displayError: errorString description: descriptionString];
}

- (void) displayError: (NSString *) errorString 
                description: (NSString *) descriptionString
{
    NSPanel *errorPanel = NSGetAlertPanel (errorString, descriptionString, 
                                                NSLocalizedString (@"KLStringOK", NULL), NULL, NULL);
    [self displayAndRunWindow: errorPanel];
    NSReleaseAlertPanel (errorPanel);
}

- (BOOL) askYesNoQuestion: (NSString *) question
{
    NSWindow *parentWindow = [self frontWindow];
    NSString *key = @"KLStringUnknownError";
    int response;
    
    if (parentWindow == loginWindow) {
        key = @"KLStringLoginFailed";
    } else if (parentWindow == changePasswordWindow) {
        key = @"KLStringChangePasswordFailed";
    } else if (parentWindow == prompterWindow) {
        key = @"KLStringPrompterFailed";
    }
    
    
    NSPanel *questionPanel = NSGetAlertPanel (NSLocalizedString (key, NULL), 
                                              question, 
                                              NSLocalizedString (@"KLStringYes", NULL), 
                                              NSLocalizedString (@"KLStringNo", NULL), 
                                              NULL);
    response = [self displayAndRunWindow: questionPanel];
    NSReleaseAlertPanel (questionPanel);
    return (response == NSAlertDefaultReturn) ? YES : NO;
}

#pragma mark -

- (int) displayAndRunWindow: (NSWindow *) window
{
    NSWindow *parent = [NSApp modalWindow];
    int response = 0;
    
    // Can't display two sheets on the same window so make it a modal dialog
    if (parent != NULL && [parent attachedSheet] != NULL) {
        dprintf ("Can't display a sheet on a sheet");
        parent = NULL;
    }
    
    // Bring KLS to the front
    [NSApp activateIgnoringOtherApps: YES];
    
    // Prepare the window:
    if (parent != NULL) {
         [NSApp beginSheet: window 
                modalForWindow: parent 
                modalDelegate: self
                didEndSelector: @selector(sheetDidEnd:returnCode:contextInfo:)
                contextInfo: window];
    } else {
        [window center];
    }
    
    // Run the main event loop:
    response = [NSApp runModalForWindow: window];
        
    // Remove the window:
    [window orderOut: self];
    
    return response;
}


- (void) sheetDidEnd: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo 
{
    if (sheet == (NSWindow *) contextInfo) {
        [NSApp stopModalWithCode: returnCode];
    }
}

- (NSWindow *) frontWindow
{
    return [NSApp modalWindow];
}

@end